diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000000..8ee54e8d34
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo
diff --git a/frontend/.npmrc b/frontend/.npmrc
new file mode 100644
index 0000000000..449691b70f
--- /dev/null
+++ b/frontend/.npmrc
@@ -0,0 +1 @@
+save-exact=true
\ No newline at end of file
diff --git a/frontend/.prettierignore b/frontend/.prettierignore
new file mode 100644
index 0000000000..940e3fdd28
--- /dev/null
+++ b/frontend/.prettierignore
@@ -0,0 +1,2 @@
+**/*.svg
+**/mockServiceWorker.js
\ No newline at end of file
diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json
new file mode 100644
index 0000000000..6d8d37b10f
--- /dev/null
+++ b/frontend/.prettierrc.json
@@ -0,0 +1,15 @@
+{
+ "printWidth": 250,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "es5",
+ "endOfLine": "auto",
+ "overrides": [
+ {
+ "files": "public/js/app.constants.js",
+ "options": {
+ "singleQuote": true
+ }
+ }
+ ]
+}
diff --git a/frontend/env.d.ts b/frontend/env.d.ts
new file mode 100644
index 0000000000..11f02fe2a0
--- /dev/null
+++ b/frontend/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
new file mode 100644
index 0000000000..e5da58a06e
--- /dev/null
+++ b/frontend/eslint.config.mjs
@@ -0,0 +1,29 @@
+import globals from "globals";
+import pluginJs from "@eslint/js";
+import tseslint from "typescript-eslint";
+import pluginVue from "eslint-plugin-vue";
+import pluginPromise from "eslint-plugin-promise";
+import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
+
+export default tseslint.config(
+ {
+ ignores: ["node_modules/**", "dist/**", "public/js/app.constants.js", "public/mockServiceWorker.js"],
+ },
+ {
+ files: ["**/*.{js,mjs,ts,vue}"],
+ languageOptions: { globals: globals.browser, ecmaVersion: "latest", parserOptions: { parser: tseslint.parser } },
+ extends: [pluginJs.configs.recommended, ...tseslint.configs.recommended, ...pluginVue.configs["flat/essential"], pluginPromise.configs["flat/recommended"], eslintPluginPrettierRecommended],
+ rules: {
+ "no-duplicate-imports": "error",
+ "promise/prefer-await-to-then": "error",
+ "require-await": "error",
+ "no-await-in-loop": "warn",
+ "prefer-rest-params": "error",
+ "prefer-spread": "error",
+ "no-var": "error",
+ "prefer-const": "error",
+ eqeqeq: ["error", "smart"],
+ "no-throw-literal": "warn",
+ },
+ }
+);
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000000..3d5413ce32
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ ServicePulse
+
+
+
+
+
+
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000000..285a1dc94d
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,9090 @@
+{
+ "name": "service-pulse",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "service-pulse",
+ "version": "1.0.0",
+ "dependencies": {
+ "@codemirror/lang-json": "6.0.2",
+ "@codemirror/lang-xml": "6.1.0",
+ "@codemirror/legacy-modes": "6.5.2",
+ "@dagrejs/dagre": "1.1.8",
+ "@fortawesome/fontawesome-svg-core": "7.1.0",
+ "@fortawesome/free-brands-svg-icons": "7.1.0",
+ "@fortawesome/free-regular-svg-icons": "7.1.0",
+ "@fortawesome/free-solid-svg-icons": "7.1.0",
+ "@fortawesome/vue-fontawesome": "3.1.2",
+ "@tinyhttp/content-disposition": "2.2.2",
+ "@vue-flow/controls": "1.1.3",
+ "@vue-flow/core": "1.47.0",
+ "@vuepic/vue-datepicker": "12.0.5",
+ "@vueuse/core": "14.0.0",
+ "bootstrap": "5.3.8",
+ "codemirror": "6.0.2",
+ "diff": "8.0.2",
+ "hex-to-css-filter": "6.0.0",
+ "lossless-json": "4.3.0",
+ "moment": "2.30.1",
+ "pinia": "3.0.4",
+ "vue": "3.5.24",
+ "vue-codemirror6": "1.4.1",
+ "vue-router": "4.6.3",
+ "vue-tippy": "6.7.1",
+ "vue-toastification": "2.0.0-rc.5",
+ "vue3-cookies": "1.0.6",
+ "vue3-simple-typeahead": "1.0.11",
+ "xml-formatter": "3.6.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.39.1",
+ "@pinia/testing": "1.0.3",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/jest-dom": "6.9.1",
+ "@testing-library/user-event": "14.6.1",
+ "@testing-library/vue": "8.1.0",
+ "@tsconfig/node18": "18.2.6",
+ "@types/bootstrap": "5.2.10",
+ "@types/jsdom": "27.0.0",
+ "@types/node": "24.10.1",
+ "@vitejs/plugin-vue": "6.0.2",
+ "@vitest/coverage-v8": "4.0.10",
+ "@vue/tsconfig": "0.8.1",
+ "cross-env": "10.1.0",
+ "eslint": "9.39.1",
+ "eslint-config-prettier": "10.1.8",
+ "eslint-plugin-prettier": "5.5.4",
+ "eslint-plugin-promise": "7.2.1",
+ "eslint-plugin-vue": "10.5.1",
+ "globals": "16.5.0",
+ "jsdom": "26.1.0",
+ "msw": "2.12.2",
+ "prettier": "3.6.2",
+ "typescript": "5.9.3",
+ "typescript-eslint": "8.47.0",
+ "vite": "7.2.2",
+ "vite-plugin-checker": "0.11.0",
+ "vite-plugin-vue-devtools": "8.0.5",
+ "vitest": "4.0.10",
+ "vue-tsc": "3.1.4"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
+ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.4",
+ "@babel/types": "^7.28.4",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
+ "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.28.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-proposal-decorators": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz",
+ "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-decorators": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz",
+ "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-attributes": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz",
+ "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz",
+ "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
+ "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.18.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz",
+ "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
+ "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/lang-json": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
+ "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/json": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-xml": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz",
+ "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.4.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/xml": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.11.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
+ "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.1.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/legacy-modes": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.2.tgz",
+ "integrity": "sha512-/jJbwSTazlQEDOQw2FJ8LEEKVS72pU0lx6oM54kGpL8t/NJ2Jda3CZ4pcltiKTdqYSRk3ug1B3pil1gsjA6+8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.8.5",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz",
+ "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.35.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.5.11",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
+ "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
+ "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.38.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
+ "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/state": "^6.5.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@dagrejs/dagre": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-1.1.8.tgz",
+ "integrity": "sha512-5SEDlndt4W/LaVzPYJW+bSmSEZc9EzTf8rJ20WCKvjS5EAZAN0b+x0Yww7VMT4R3Wootkg+X9bUfUxazYw6Blw==",
+ "license": "MIT",
+ "dependencies": {
+ "@dagrejs/graphlib": "2.2.4"
+ }
+ },
+ "node_modules/@dagrejs/graphlib": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-2.2.4.tgz",
+ "integrity": "sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">17.0.0"
+ }
+ },
+ "node_modules/@date-fns/tz": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
+ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
+ "license": "MIT"
+ },
+ "node_modules/@epic-web/invariant": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
+ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "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": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "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.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "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.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": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+ "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.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": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "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.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@floating-ui/vue": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/vue/-/vue-1.1.9.tgz",
+ "integrity": "sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4",
+ "@floating-ui/utils": "^0.2.10",
+ "vue-demi": ">=0.13.0"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
+ "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
+ "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
+ "integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz",
+ "integrity": "sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz",
+ "integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/vue-fontawesome": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.1.2.tgz",
+ "integrity": "sha512-mhYnBIuuW8OIMHf31kOjaBmyE7BMrwBorhrOHVud6vTTu+7IPQNWB+DWaHoE75v10dRF5s/dFtcrgE7vKSEWwQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
+ "vue": ">= 3.0.0 < 4"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.6",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz",
+ "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
+ "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@inquirer/ansi": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
+ "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.21",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
+ "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.3.2",
+ "@inquirer/type": "^3.0.10"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.3.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
+ "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/ansi": "^1.0.2",
+ "@inquirer/figures": "^1.0.15",
+ "@inquirer/type": "^3.0.10",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@inquirer/core/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.15",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
+ "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
+ "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@isaacs/balanced-match": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+ "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/brace-expansion": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+ "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@isaacs/balanced-match": "^4.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz",
+ "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz",
+ "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/json": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
+ "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz",
+ "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/xml": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz",
+ "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
+ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
+ "license": "MIT"
+ },
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.40.0",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz",
+ "integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "node_modules/@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@pinia/testing": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@pinia/testing/-/testing-1.0.3.tgz",
+ "integrity": "sha512-g+qR49GNdI1Z8rZxKrQC3GN+LfnGTNf5Kk8Nz5Cz6mIGva5WRS+ffPXQfzhA0nu6TveWzPNYTjGl4nJqd3Cu9Q==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "pinia": ">=3.0.4"
+ }
+ },
+ "node_modules/@pkgr/core": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
+ "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/pkgr"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz",
+ "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.48.1.tgz",
+ "integrity": "sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.48.1.tgz",
+ "integrity": "sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.1.tgz",
+ "integrity": "sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.1.tgz",
+ "integrity": "sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.48.1.tgz",
+ "integrity": "sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.48.1.tgz",
+ "integrity": "sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.48.1.tgz",
+ "integrity": "sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.48.1.tgz",
+ "integrity": "sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.48.1.tgz",
+ "integrity": "sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.48.1.tgz",
+ "integrity": "sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.48.1.tgz",
+ "integrity": "sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.48.1.tgz",
+ "integrity": "sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.48.1.tgz",
+ "integrity": "sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.48.1.tgz",
+ "integrity": "sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.48.1.tgz",
+ "integrity": "sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.48.1.tgz",
+ "integrity": "sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.48.1.tgz",
+ "integrity": "sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.48.1.tgz",
+ "integrity": "sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.48.1.tgz",
+ "integrity": "sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.48.1.tgz",
+ "integrity": "sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+ "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@testing-library/vue": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@testing-library/vue/-/vue-8.1.0.tgz",
+ "integrity": "sha512-ls4RiHO1ta4mxqqajWRh8158uFObVrrtAPoxk7cIp4HrnQUj/ScKzqz53HxYpG3X6Zb7H2v+0eTGLSoy8HQ2nA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "@testing-library/dom": "^9.3.3",
+ "@vue/test-utils": "^2.4.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "peerDependencies": {
+ "@vue/compiler-sfc": ">= 3",
+ "vue": ">= 3"
+ },
+ "peerDependenciesMeta": {
+ "@vue/compiler-sfc": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/vue/node_modules/@testing-library/dom": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
+ "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.1.3",
+ "chalk": "^4.1.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@testing-library/vue/node_modules/aria-query": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
+ "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "deep-equal": "^2.0.5"
+ }
+ },
+ "node_modules/@tinyhttp/content-disposition": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz",
+ "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1"
+ }
+ },
+ "node_modules/@tsconfig/node18": {
+ "version": "18.2.6",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.6.tgz",
+ "integrity": "sha512-eAWQzAjPj18tKnDzmWstz4OyWewLUNBm9tdoN9LayzoboRktYx3Enk1ZXPmThj55L7c4VWYq/Bzq0A51znZfhw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/bootstrap": {
+ "version": "5.2.10",
+ "resolved": "https://registry.npmjs.org/@types/bootstrap/-/bootstrap-5.2.10.tgz",
+ "integrity": "sha512-F2X+cd6551tep0MvVZ6nM8v7XgGN/twpdNDjqS1TUM7YFNEtQYWk+dKAnH+T1gr6QgCoGMPl487xw/9hXooa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@popperjs/core": "^2.9.2"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/jsdom": {
+ "version": "27.0.0",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz",
+ "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "parse5": "^7.0.0"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
+ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/statuses": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
+ "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
+ "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.10.0",
+ "@typescript-eslint/scope-manager": "8.47.0",
+ "@typescript-eslint/type-utils": "8.47.0",
+ "@typescript-eslint/utils": "8.47.0",
+ "@typescript-eslint/visitor-keys": "8.47.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^7.0.0",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.47.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz",
+ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.47.0",
+ "@typescript-eslint/types": "8.47.0",
+ "@typescript-eslint/typescript-estree": "8.47.0",
+ "@typescript-eslint/visitor-keys": "8.47.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
+ "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.47.0",
+ "@typescript-eslint/types": "^8.47.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
+ "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.47.0",
+ "@typescript-eslint/visitor-keys": "8.47.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
+ "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz",
+ "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.47.0",
+ "@typescript-eslint/typescript-estree": "8.47.0",
+ "@typescript-eslint/utils": "8.47.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
+ "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
+ "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.47.0",
+ "@typescript-eslint/tsconfig-utils": "8.47.0",
+ "@typescript-eslint/types": "8.47.0",
+ "@typescript-eslint/visitor-keys": "8.47.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
+ "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.7.0",
+ "@typescript-eslint/scope-manager": "8.47.0",
+ "@typescript-eslint/types": "8.47.0",
+ "@typescript-eslint/typescript-estree": "8.47.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
+ "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.47.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
+ "integrity": "sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.50"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.10.tgz",
+ "integrity": "sha512-g+brmtoKa/sAeIohNJnnWhnHtU6GuqqVOSQ4SxDIPcgZWZyhJs5RmF5LpqXs8Kq64lANP+vnbn5JLzhLj/G56g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.0.10",
+ "ast-v8-to-istanbul": "^0.3.8",
+ "debug": "^4.4.3",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-lib-source-maps": "^5.0.6",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.1",
+ "std-env": "^3.10.0",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.0.10",
+ "vitest": "4.0.10"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.10.tgz",
+ "integrity": "sha512-3QkTX/lK39FBNwARCQRSQr0TP9+ywSdxSX+LgbJ2M1WmveXP72anTbnp2yl5fH+dU6SUmBzNMrDHs80G8G2DZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.10",
+ "@vitest/utils": "4.0.10",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.10.tgz",
+ "integrity": "sha512-e2OfdexYkjkg8Hh3L9NVEfbwGXq5IZbDovkf30qW2tOh7Rh9sVtmSr2ztEXOFbymNxS4qjzLXUQIvATvN4B+lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.10",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.10.tgz",
+ "integrity": "sha512-99EQbpa/zuDnvVjthwz5bH9o8iPefoQZ63WV8+bsRJZNw3qQSvSltfut8yu1Jc9mqOYi7pEbsKxYTi/rjaq6PA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.10.tgz",
+ "integrity": "sha512-EXU2iSkKvNwtlL8L8doCpkyclw0mc/t4t9SeOnfOFPyqLmQwuceMPA4zJBa6jw0MKsZYbw7kAn+gl7HxrlB8UQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.10",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.10.tgz",
+ "integrity": "sha512-2N4X2ZZl7kZw0qeGdQ41H0KND96L3qX1RgwuCfy6oUsF2ISGD/HpSbmms+CkIOsQmg2kulwfhJ4CI0asnZlvkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.10",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.10.tgz",
+ "integrity": "sha512-AsY6sVS8OLb96GV5RoG8B6I35GAbNrC49AO+jNRF9YVGb/g9t+hzNm1H6kD0NDp8tt7VJLs6hb7YMkDXqu03iw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.10.tgz",
+ "integrity": "sha512-kOuqWnEwZNtQxMKg3WmPK1vmhZu9WcoX69iwWjVz+jvKTsF1emzsv3eoPcDr6ykA3qP2bsCQE7CwqfNtAVzsmg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.10",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.23",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz",
+ "integrity": "sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.23"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.23",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.23.tgz",
+ "integrity": "sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.23",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.23.tgz",
+ "integrity": "sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.23",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue-flow/controls": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.3.tgz",
+ "integrity": "sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@vue-flow/core": "^1.23.0",
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/@vue-flow/core": {
+ "version": "1.47.0",
+ "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.47.0.tgz",
+ "integrity": "sha512-w+qrm/xjQP5NUeKUOMIbQvpOeivTbGZtY2lGffK5kHiN3ZLyEazhESc8OeIV9NZkK2T5DIeyX/nhHxCC45HLiw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vueuse/core": "^10.5.0",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.3.0"
+ }
+ },
+ "node_modules/@vue-flow/core/node_modules/@types/web-bluetooth": {
+ "version": "0.0.20",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
+ "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
+ "license": "MIT"
+ },
+ "node_modules/@vue-flow/core/node_modules/@vueuse/core": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz",
+ "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.20",
+ "@vueuse/metadata": "10.11.1",
+ "@vueuse/shared": "10.11.1",
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vue-flow/core/node_modules/@vueuse/metadata": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz",
+ "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vue-flow/core/node_modules/@vueuse/shared": {
+ "version": "10.11.1",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz",
+ "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": ">=0.14.8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vue/babel-helper-vue-transform-on": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz",
+ "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/babel-plugin-jsx": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz",
+ "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.0",
+ "@babel/types": "^7.28.2",
+ "@vue/babel-helper-vue-transform-on": "1.5.0",
+ "@vue/babel-plugin-resolve-type": "1.5.0",
+ "@vue/shared": "^3.5.18"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/babel-plugin-resolve-type": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz",
+ "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/parser": "^7.28.0",
+ "@vue/compiler-sfc": "^3.5.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.24.tgz",
+ "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/shared": "3.5.24",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-core/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@vue/compiler-core/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/@vue/compiler-dom": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz",
+ "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.24",
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz",
+ "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@vue/compiler-core": "3.5.24",
+ "@vue/compiler-dom": "3.5.24",
+ "@vue/compiler-ssr": "3.5.24",
+ "@vue/shared": "3.5.24",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-sfc/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/@vue/compiler-ssr": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz",
+ "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.24",
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "7.7.7",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
+ "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^7.7.7"
+ }
+ },
+ "node_modules/@vue/devtools-core": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.0.5.tgz",
+ "integrity": "sha512-dpCw8nl0GDBuiL9SaY0mtDxoGIEmU38w+TQiYEPOLhW03VDC0lfNMYXS/qhl4I0YlysGp04NLY4UNn6xgD0VIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^8.0.5",
+ "@vue/devtools-shared": "^8.0.5",
+ "mitt": "^3.0.1",
+ "nanoid": "^5.1.5",
+ "pathe": "^2.0.3",
+ "vite-hot-client": "^2.1.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/@vue/devtools-core/node_modules/@vue/devtools-kit": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz",
+ "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^8.0.5",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^2.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/@vue/devtools-core/node_modules/@vue/devtools-shared": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz",
+ "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/@vue/devtools-core/node_modules/nanoid": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz",
+ "integrity": "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/@vue/devtools-core/node_modules/perfect-debounce": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
+ "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "7.7.7",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
+ "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^7.7.7",
+ "birpc": "^2.3.0",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^1.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "7.7.7",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
+ "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/@vue/language-core": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.1.4.tgz",
+ "integrity": "sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.23",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^3.0.0",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1",
+ "picomatch": "^4.0.2"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/language-core/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/@vue/reactivity": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
+ "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
+ "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.24",
+ "@vue/shared": "3.5.24"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
+ "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.24",
+ "@vue/runtime-core": "3.5.24",
+ "@vue/shared": "3.5.24",
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
+ "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.24",
+ "@vue/shared": "3.5.24"
+ },
+ "peerDependencies": {
+ "vue": "3.5.24"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.24.tgz",
+ "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/test-utils": {
+ "version": "2.4.6",
+ "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz",
+ "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-beautify": "^1.14.9",
+ "vue-component-type-helpers": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/tsconfig": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz",
+ "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "typescript": "5.x",
+ "vue": "^3.4.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vuepic/vue-datepicker": {
+ "version": "12.0.5",
+ "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-12.0.5.tgz",
+ "integrity": "sha512-kxvJOIcU6ZuEIt4Yvo9QVNiPIKCT9cqZiQfz244ojkCwHHOCrmssjRxX4GyZ1may3snXm5XM9c2Vk1G0RXtClQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@date-fns/tz": "^1.4.1",
+ "@floating-ui/vue": "^1.1.9",
+ "@vueuse/core": "^14.0.0",
+ "date-fns": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "vue": ">=3.5.0"
+ }
+ },
+ "node_modules/@vueuse/core": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.0.0.tgz",
+ "integrity": "sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "14.0.0",
+ "@vueuse/shared": "14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.0.0.tgz",
+ "integrity": "sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.0.0.tgz",
+ "integrity": "sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.0.tgz",
+ "integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ansis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
+ "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.8.tgz",
+ "integrity": "sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^9.0.1"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.14.tgz",
+ "integrity": "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.6.1.tgz",
+ "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/bootstrap": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
+ "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "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": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.26.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
+ "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.9",
+ "caniuse-lite": "^1.0.30001746",
+ "electron-to-chromium": "^1.5.227",
+ "node-releases": "^2.0.21",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001749",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
+ "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
+ "integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/codemirror": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
+ "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/copy-anything": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
+ "license": "MIT"
+ },
+ "node_modules/cross-env": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
+ "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@epic-web/invariant": "^1.0.0",
+ "cross-spawn": "^7.0.6"
+ },
+ "bin": {
+ "cross-env": "dist/bin/cross-env.js",
+ "cross-env-shell": "dist/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "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==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-equal": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
+ "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.5",
+ "es-get-iterator": "^1.1.3",
+ "get-intrinsic": "^1.2.2",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.2",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/default-browser": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/diff": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
+ "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+ "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/editorconfig/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/editorconfig/node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.233",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz",
+ "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/error-stack-parser-es": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@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.1",
+ "@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",
+ "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.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",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "5.5.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
+ "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0",
+ "synckit": "^0.11.7"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-plugin-prettier"
+ },
+ "peerDependencies": {
+ "@types/eslint": ">=8.0.0",
+ "eslint": ">=8.0.0",
+ "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
+ "prettier": ">=3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/eslint": {
+ "optional": true
+ },
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-promise": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz",
+ "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-vue": {
+ "version": "10.5.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.5.1.tgz",
+ "integrity": "sha512-SbR9ZBUFKgvWAbq3RrdCtWaW0IKm6wwUiApxf3BVTNfqUIo4IQQmreMg2iHFJJ6C/0wss3LXURBJ1OwS/MhFcQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "natural-compare": "^1.4.0",
+ "nth-check": "^2.1.1",
+ "postcss-selector-parser": "^6.0.15",
+ "semver": "^7.6.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "peerDependencies": {
+ "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
+ "@typescript-eslint/parser": "^7.0.0 || ^8.0.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "vue-eslint-parser": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@stylistic/eslint-plugin": {
+ "optional": true
+ },
+ "@typescript-eslint/parser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "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": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "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": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "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.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
+ "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-12.0.0.tgz",
+ "integrity": "sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.1.1",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/graphql": {
+ "version": "16.12.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
+ "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/headers-polyfill": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hex-to-css-filter": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/hex-to-css-filter/-/hex-to-css-filter-6.0.0.tgz",
+ "integrity": "sha512-nQMSn+lEF2C1ddyMVBIyIPWRnaH0ZwFi2dcTtAop6cUyY0AbkA6uI/evpkvEHMD9G9DlHf8xraMJIIJ74S+MKA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=6.10.2"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
+ "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.23",
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
+ "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+ "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.4.2",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.1"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "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": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kolorist": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
+ "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lossless-json": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.3.0.tgz",
+ "integrity": "sha512-ToxOC+SsduRmdSuoLZLYAr5zy1Qu7l5XhmPWM3zefCZ5IcrzW/h108qbJUKfOlDlhvhjUK84+8PSVX0kxnit0g==",
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz",
+ "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "license": "MIT"
+ },
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/msw": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.2.tgz",
+ "integrity": "sha512-Fsr8AR5Yu6C0thoWa1Z8qGBFQLDvLsWlAn/v3CNLiUizoRqBYArK3Ex3thXpMWRr1Li5/MKLOEZ5mLygUmWi1A==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/confirm": "^5.0.0",
+ "@mswjs/interceptors": "^0.40.0",
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@types/statuses": "^2.0.4",
+ "cookie": "^1.0.2",
+ "graphql": "^16.8.1",
+ "headers-polyfill": "^4.0.2",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "path-to-regexp": "^6.3.0",
+ "picocolors": "^1.1.1",
+ "rettime": "^0.7.0",
+ "statuses": "^2.0.2",
+ "strict-event-emitter": "^0.5.1",
+ "tough-cookie": "^6.0.0",
+ "type-fest": "^4.26.1",
+ "until-async": "^3.0.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "msw": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mswjs"
+ },
+ "peerDependencies": {
+ "typescript": ">= 4.8.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/msw/node_modules/tldts": {
+ "version": "7.0.18",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.18.tgz",
+ "integrity": "sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.0.18"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/msw/node_modules/tldts-core": {
+ "version": "7.0.18",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.18.tgz",
+ "integrity": "sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/msw/node_modules/tough-cookie": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
+ "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.23",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
+ "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^4.0.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/npm-run-path/node_modules/path-key": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
+ "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.21",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz",
+ "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/open": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "wsl-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
+ "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pinia": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
+ "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/devtools-api": "^7.7.7"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.5.0",
+ "vue": "^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
+ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
+ "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rettime": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz",
+ "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.48.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz",
+ "integrity": "sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.48.1",
+ "@rollup/rollup-android-arm64": "4.48.1",
+ "@rollup/rollup-darwin-arm64": "4.48.1",
+ "@rollup/rollup-darwin-x64": "4.48.1",
+ "@rollup/rollup-freebsd-arm64": "4.48.1",
+ "@rollup/rollup-freebsd-x64": "4.48.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.48.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.48.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.48.1",
+ "@rollup/rollup-linux-arm64-musl": "4.48.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.48.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.48.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.48.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.48.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.48.1",
+ "@rollup/rollup-linux-x64-gnu": "4.48.1",
+ "@rollup/rollup-linux-x64-musl": "4.48.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.48.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.48.1",
+ "@rollup/rollup-win32-x64-msvc": "4.48.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi/node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
+ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/superjson": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
+ "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "copy-anything": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/synckit": {
+ "version": "0.11.11",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
+ "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@pkgr/core": "^0.2.9"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/synckit"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "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",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
+ "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tippy.js": {
+ "version": "6.3.7",
+ "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
+ "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@popperjs/core": "^2.9.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.47.0.tgz",
+ "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.47.0",
+ "@typescript-eslint/parser": "8.47.0",
+ "@typescript-eslint/typescript-estree": "8.47.0",
+ "@typescript-eslint/utils": "8.47.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unplugin-utils": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+ "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
+ "node_modules/unplugin-utils/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/until-async": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
+ "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/kettanaito"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
+ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-dev-rpc": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz",
+ "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "birpc": "^2.4.0",
+ "vite-hot-client": "^2.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0"
+ }
+ },
+ "node_modules/vite-hot-client": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz",
+ "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
+ }
+ },
+ "node_modules/vite-plugin-checker": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.11.0.tgz",
+ "integrity": "sha512-iUdO9Pl9UIBRPAragwi3as/BXXTtRu4G12L3CMrjx+WVTd9g/MsqNakreib9M/2YRVkhZYiTEwdH2j4Dm0w7lw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "chokidar": "^4.0.3",
+ "npm-run-path": "^6.0.0",
+ "picocolors": "^1.1.1",
+ "picomatch": "^4.0.3",
+ "tiny-invariant": "^1.3.3",
+ "tinyglobby": "^0.2.14",
+ "vscode-uri": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=16.11"
+ },
+ "peerDependencies": {
+ "@biomejs/biome": ">=1.7",
+ "eslint": ">=7",
+ "meow": "^13.2.0",
+ "optionator": "^0.9.4",
+ "oxlint": ">=1",
+ "stylelint": ">=16",
+ "typescript": "*",
+ "vite": ">=5.4.20",
+ "vls": "*",
+ "vti": "*",
+ "vue-tsc": "~2.2.10 || ^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@biomejs/biome": {
+ "optional": true
+ },
+ "eslint": {
+ "optional": true
+ },
+ "meow": {
+ "optional": true
+ },
+ "optionator": {
+ "optional": true
+ },
+ "oxlint": {
+ "optional": true
+ },
+ "stylelint": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ },
+ "vls": {
+ "optional": true
+ },
+ "vti": {
+ "optional": true
+ },
+ "vue-tsc": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-checker/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/vite-plugin-inspect": {
+ "version": "11.3.3",
+ "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz",
+ "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansis": "^4.1.0",
+ "debug": "^4.4.1",
+ "error-stack-parser-es": "^1.0.5",
+ "ohash": "^2.0.11",
+ "open": "^10.2.0",
+ "perfect-debounce": "^2.0.0",
+ "sirv": "^3.0.1",
+ "unplugin-utils": "^0.3.0",
+ "vite-dev-rpc": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@nuxt/kit": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-inspect/node_modules/perfect-debounce": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
+ "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite-plugin-vue-devtools": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.0.5.tgz",
+ "integrity": "sha512-p619BlKFOqQXJ6uDWS1vUPQzuJOD6xJTfftj57JXBGoBD/yeQCowR7pnWcr/FEX4/HVkFbreI6w2uuGBmQOh6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-core": "^8.0.5",
+ "@vue/devtools-kit": "^8.0.5",
+ "@vue/devtools-shared": "^8.0.5",
+ "sirv": "^3.0.2",
+ "vite-plugin-inspect": "^11.3.3",
+ "vite-plugin-vue-inspector": "^5.3.2"
+ },
+ "engines": {
+ "node": ">=v14.21.3"
+ },
+ "peerDependencies": {
+ "vite": "^6.0.0 || ^7.0.0-0"
+ }
+ },
+ "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-kit": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.0.5.tgz",
+ "integrity": "sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^8.0.5",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^2.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/vite-plugin-vue-devtools/node_modules/@vue/devtools-shared": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.0.5.tgz",
+ "integrity": "sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/vite-plugin-vue-devtools/node_modules/perfect-debounce": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz",
+ "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vite-plugin-vue-inspector": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz",
+ "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.0",
+ "@babel/plugin-proposal-decorators": "^7.23.0",
+ "@babel/plugin-syntax-import-attributes": "^7.22.5",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-transform-typescript": "^7.22.15",
+ "@vue/babel-plugin-jsx": "^1.1.5",
+ "@vue/compiler-dom": "^3.3.4",
+ "kolorist": "^1.8.0",
+ "magic-string": "^0.30.4"
+ },
+ "peerDependencies": {
+ "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0"
+ }
+ },
+ "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",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.10.tgz",
+ "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vitest/expect": "4.0.10",
+ "@vitest/mocker": "4.0.10",
+ "@vitest/pretty-format": "4.0.10",
+ "@vitest/runner": "4.0.10",
+ "@vitest/snapshot": "4.0.10",
+ "@vitest/spy": "4.0.10",
+ "@vitest/utils": "4.0.10",
+ "debug": "^4.4.3",
+ "es-module-lexer": "^1.7.0",
+ "expect-type": "^1.2.2",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^3.10.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.0.10",
+ "@vitest/browser-preview": "4.0.10",
+ "@vitest/browser-webdriverio": "4.0.10",
+ "@vitest/ui": "4.0.10",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/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/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue": {
+ "version": "3.5.24",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.24.tgz",
+ "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.24",
+ "@vue/compiler-sfc": "3.5.24",
+ "@vue/runtime-dom": "3.5.24",
+ "@vue/server-renderer": "3.5.24",
+ "@vue/shared": "3.5.24"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-codemirror6": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/vue-codemirror6/-/vue-codemirror6-1.4.1.tgz",
+ "integrity": "sha512-mokK4q89TvxtGXzdEv3YyvfX3RJJs7VxyNjBNpdNbP+jpum/ttLvWB+TEh/ziVZq5mFrQazVPCfUV8TVI/Ji2A==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": "latest"
+ },
+ "engines": {
+ "node": ">=18",
+ "pnpm": ">=10.3.0"
+ },
+ "peerDependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "codemirror": "^6.0.0",
+ "style-mod": "^4.0.0",
+ "vue": "^2.7.14 || ^3.3.4"
+ }
+ },
+ "node_modules/vue-component-type-helpers": {
+ "version": "2.2.12",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz",
+ "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-eslint-parser": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz",
+ "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "debug": "^4.4.0",
+ "eslint-scope": "^8.2.0",
+ "eslint-visitor-keys": "^4.2.0",
+ "espree": "^10.3.0",
+ "esquery": "^1.6.0",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
+ "integrity": "sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/vue-router/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/vue-tippy": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/vue-tippy/-/vue-tippy-6.7.1.tgz",
+ "integrity": "sha512-gdHbBV5/Vc8gH87hQHLA7TN1K4BlLco3MAPrTb70ZYGXxx+55rAU4a4mt0fIoP+gB3etu1khUZ6c29Br1n0CiA==",
+ "license": "MIT",
+ "dependencies": {
+ "tippy.js": "^6.3.7"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/vue-toastification": {
+ "version": "2.0.0-rc.5",
+ "resolved": "https://registry.npmjs.org/vue-toastification/-/vue-toastification-2.0.0-rc.5.tgz",
+ "integrity": "sha512-q73e5jy6gucEO/U+P48hqX+/qyXDozAGmaGgLFm5tXX4wJBcVsnGp4e/iJqlm9xzHETYOilUuwOUje2Qg1JdwA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.0.2"
+ }
+ },
+ "node_modules/vue-tsc": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.1.4.tgz",
+ "integrity": "sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@volar/typescript": "2.4.23",
+ "@vue/language-core": "3.1.4"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
+ }
+ },
+ "node_modules/vue3-cookies": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/vue3-cookies/-/vue3-cookies-1.0.6.tgz",
+ "integrity": "sha512-a1UvVD0qIgxyOqjlSOwnLnqAnz8ASltugEv8yX+96i/WGZAN9fEDci7xO4HIWZE1uToUnRq9JnFhvfDCSo45OA==",
+ "license": "MIT",
+ "dependencies": {
+ "vue": "^3.0.0"
+ }
+ },
+ "node_modules/vue3-simple-typeahead": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/vue3-simple-typeahead/-/vue3-simple-typeahead-1.0.11.tgz",
+ "integrity": "sha512-JKgVfWz7uENBoB4ginYw28ZVlv3LNTkFbZzbDdK2W0y+Pj2xIryfwKlkW5bYUN+w4CiPS6CmXhPma9uwUnk0Lg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "vue": "^3.0.5"
+ }
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wsl-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/xml-formatter": {
+ "version": "3.6.7",
+ "resolved": "https://registry.npmjs.org/xml-formatter/-/xml-formatter-3.6.7.tgz",
+ "integrity": "sha512-IsfFYJQuoDqtUlKhm4EzeoBOb+fQwzQVeyxxAQ0sThn/nFnQmyLPTplqq4yRhaOENH/tAyujD2TBfIYzUKB6hg==",
+ "license": "MIT",
+ "dependencies": {
+ "xml-parser-xo": "^4.1.5"
+ },
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xml-parser-xo": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/xml-parser-xo/-/xml-parser-xo-4.1.5.tgz",
+ "integrity": "sha512-TxyRxk9sTOUg3glxSIY6f0nfuqRll2OEF8TspLgh5mZkLuBgheCn3zClcDSGJ58TvNmiwyCCuat4UajPud/5Og==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
+ "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000000..91cf4b7df9
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,90 @@
+{
+ "name": "service-pulse",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Production monitoring for distributed systems.",
+ "main": "index.html",
+ "scripts": {
+ "dev": "vite",
+ "dev:mocks": "cross-env NODE_ENV=dev-mocks vite",
+ "build": "vite build",
+ "type-check": "vue-tsc --build --force",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "test:component": "vitest run ./src",
+ "test:coverage": "vitest run --coverage",
+ "test:application:vitest": "npx vitest run ./test/specs",
+ "test:application": "npm run test:application:vitest"
+ },
+ "dependencies": {
+ "@codemirror/lang-json": "6.0.2",
+ "@codemirror/lang-xml": "6.1.0",
+ "@codemirror/legacy-modes": "6.5.2",
+ "@dagrejs/dagre": "1.1.8",
+ "@fortawesome/fontawesome-svg-core": "7.1.0",
+ "@fortawesome/free-brands-svg-icons": "7.1.0",
+ "@fortawesome/free-regular-svg-icons": "7.1.0",
+ "@fortawesome/free-solid-svg-icons": "7.1.0",
+ "@fortawesome/vue-fontawesome": "3.1.2",
+ "@tinyhttp/content-disposition": "2.2.2",
+ "@vue-flow/controls": "1.1.3",
+ "@vue-flow/core": "1.47.0",
+ "@vuepic/vue-datepicker": "12.0.5",
+ "@vueuse/core": "14.0.0",
+ "bootstrap": "5.3.8",
+ "codemirror": "6.0.2",
+ "diff": "8.0.2",
+ "hex-to-css-filter": "6.0.0",
+ "lossless-json": "4.3.0",
+ "moment": "2.30.1",
+ "pinia": "3.0.4",
+ "vue": "3.5.24",
+ "vue-codemirror6": "1.4.1",
+ "vue-router": "4.6.3",
+ "vue-tippy": "6.7.1",
+ "vue-toastification": "2.0.0-rc.5",
+ "vue3-cookies": "1.0.6",
+ "vue3-simple-typeahead": "1.0.11",
+ "xml-formatter": "3.6.7"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.39.1",
+ "@pinia/testing": "1.0.3",
+ "@testing-library/dom": "10.4.1",
+ "@testing-library/jest-dom": "6.9.1",
+ "@testing-library/user-event": "14.6.1",
+ "@testing-library/vue": "8.1.0",
+ "@tsconfig/node18": "18.2.6",
+ "@types/bootstrap": "5.2.10",
+ "@types/jsdom": "27.0.0",
+ "@types/node": "24.10.1",
+ "@vitejs/plugin-vue": "6.0.2",
+ "@vitest/coverage-v8": "4.0.10",
+ "@vue/tsconfig": "0.8.1",
+ "cross-env": "10.1.0",
+ "eslint": "9.39.1",
+ "eslint-config-prettier": "10.1.8",
+ "eslint-plugin-prettier": "5.5.4",
+ "eslint-plugin-promise": "7.2.1",
+ "eslint-plugin-vue": "10.5.1",
+ "globals": "16.5.0",
+ "jsdom": "26.1.0",
+ "msw": "2.12.2",
+ "prettier": "3.6.2",
+ "typescript": "5.9.3",
+ "typescript-eslint": "8.47.0",
+ "vite": "7.2.2",
+ "vite-plugin-checker": "0.11.0",
+ "vite-plugin-vue-devtools": "8.0.5",
+ "vitest": "4.0.10",
+ "vue-tsc": "3.1.4"
+ },
+ "msw": {
+ "workerDirectory": [
+ "public"
+ ]
+ },
+ "overrides": {
+ "glob": "^12.0.0"
+ }
+}
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
new file mode 100644
index 0000000000..bed01835a6
Binary files /dev/null and b/frontend/public/favicon.ico differ
diff --git a/frontend/public/js/app.constants.json b/frontend/public/js/app.constants.json
new file mode 100644
index 0000000000..168cc7a8ad
--- /dev/null
+++ b/frontend/public/js/app.constants.json
@@ -0,0 +1,7 @@
+{
+ "default_route": "/dashboard",
+ "version": "1.2.0",
+ "service_control_url": "http://localhost:33333/api/",
+ "monitoring_url": "http://localhost:33633/",
+ "showPendingRetry": true
+}
diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js
new file mode 100644
index 0000000000..f5cddde042
--- /dev/null
+++ b/frontend/public/mockServiceWorker.js
@@ -0,0 +1,349 @@
+/* eslint-disable */
+/* tslint:disable */
+
+/**
+ * Mock Service Worker.
+ * @see https://github.com/mswjs/msw
+ * - Please do NOT modify this file.
+ */
+
+const PACKAGE_VERSION = '2.12.2'
+const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
+const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
+const activeClientIds = new Set()
+
+addEventListener('install', function () {
+ self.skipWaiting()
+})
+
+addEventListener('activate', function (event) {
+ event.waitUntil(self.clients.claim())
+})
+
+addEventListener('message', async function (event) {
+ const clientId = Reflect.get(event.source || {}, 'id')
+
+ if (!clientId || !self.clients) {
+ return
+ }
+
+ const client = await self.clients.get(clientId)
+
+ if (!client) {
+ return
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ switch (event.data) {
+ case 'KEEPALIVE_REQUEST': {
+ sendToClient(client, {
+ type: 'KEEPALIVE_RESPONSE',
+ })
+ break
+ }
+
+ case 'INTEGRITY_CHECK_REQUEST': {
+ sendToClient(client, {
+ type: 'INTEGRITY_CHECK_RESPONSE',
+ payload: {
+ packageVersion: PACKAGE_VERSION,
+ checksum: INTEGRITY_CHECKSUM,
+ },
+ })
+ break
+ }
+
+ case 'MOCK_ACTIVATE': {
+ activeClientIds.add(clientId)
+
+ sendToClient(client, {
+ type: 'MOCKING_ENABLED',
+ payload: {
+ client: {
+ id: client.id,
+ frameType: client.frameType,
+ },
+ },
+ })
+ break
+ }
+
+ case 'CLIENT_CLOSED': {
+ activeClientIds.delete(clientId)
+
+ const remainingClients = allClients.filter((client) => {
+ return client.id !== clientId
+ })
+
+ // Unregister itself when there are no more clients
+ if (remainingClients.length === 0) {
+ self.registration.unregister()
+ }
+
+ break
+ }
+ }
+})
+
+addEventListener('fetch', function (event) {
+ const requestInterceptedAt = Date.now()
+
+ // Bypass navigation requests.
+ if (event.request.mode === 'navigate') {
+ return
+ }
+
+ // Opening the DevTools triggers the "only-if-cached" request
+ // that cannot be handled by the worker. Bypass such requests.
+ if (
+ event.request.cache === 'only-if-cached' &&
+ event.request.mode !== 'same-origin'
+ ) {
+ return
+ }
+
+ // Bypass all requests when there are no active clients.
+ // Prevents the self-unregistered worked from handling requests
+ // after it's been terminated (still remains active until the next reload).
+ if (activeClientIds.size === 0) {
+ return
+ }
+
+ const requestId = crypto.randomUUID()
+ event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
+})
+
+/**
+ * @param {FetchEvent} event
+ * @param {string} requestId
+ * @param {number} requestInterceptedAt
+ */
+async function handleRequest(event, requestId, requestInterceptedAt) {
+ const client = await resolveMainClient(event)
+ const requestCloneForEvents = event.request.clone()
+ const response = await getResponse(
+ event,
+ client,
+ requestId,
+ requestInterceptedAt,
+ )
+
+ // Send back the response clone for the "response:*" life-cycle events.
+ // Ensure MSW is active and ready to handle the message, otherwise
+ // this message will pend indefinitely.
+ if (client && activeClientIds.has(client.id)) {
+ const serializedRequest = await serializeRequest(requestCloneForEvents)
+
+ // Clone the response so both the client and the library could consume it.
+ const responseClone = response.clone()
+
+ sendToClient(
+ client,
+ {
+ type: 'RESPONSE',
+ payload: {
+ isMockedResponse: IS_MOCKED_RESPONSE in response,
+ request: {
+ id: requestId,
+ ...serializedRequest,
+ },
+ response: {
+ type: responseClone.type,
+ status: responseClone.status,
+ statusText: responseClone.statusText,
+ headers: Object.fromEntries(responseClone.headers.entries()),
+ body: responseClone.body,
+ },
+ },
+ },
+ responseClone.body ? [serializedRequest.body, responseClone.body] : [],
+ )
+ }
+
+ return response
+}
+
+/**
+ * Resolve the main client for the given event.
+ * Client that issues a request doesn't necessarily equal the client
+ * that registered the worker. It's with the latter the worker should
+ * communicate with during the response resolving phase.
+ * @param {FetchEvent} event
+ * @returns {Promise}
+ */
+async function resolveMainClient(event) {
+ const client = await self.clients.get(event.clientId)
+
+ if (activeClientIds.has(event.clientId)) {
+ return client
+ }
+
+ if (client?.frameType === 'top-level') {
+ return client
+ }
+
+ const allClients = await self.clients.matchAll({
+ type: 'window',
+ })
+
+ return allClients
+ .filter((client) => {
+ // Get only those clients that are currently visible.
+ return client.visibilityState === 'visible'
+ })
+ .find((client) => {
+ // Find the client ID that's recorded in the
+ // set of clients that have registered the worker.
+ return activeClientIds.has(client.id)
+ })
+}
+
+/**
+ * @param {FetchEvent} event
+ * @param {Client | undefined} client
+ * @param {string} requestId
+ * @param {number} requestInterceptedAt
+ * @returns {Promise}
+ */
+async function getResponse(event, client, requestId, requestInterceptedAt) {
+ // Clone the request because it might've been already used
+ // (i.e. its body has been read and sent to the client).
+ const requestClone = event.request.clone()
+
+ function passthrough() {
+ // Cast the request headers to a new Headers instance
+ // so the headers can be manipulated with.
+ const headers = new Headers(requestClone.headers)
+
+ // Remove the "accept" header value that marked this request as passthrough.
+ // This prevents request alteration and also keeps it compliant with the
+ // user-defined CORS policies.
+ const acceptHeader = headers.get('accept')
+ if (acceptHeader) {
+ const values = acceptHeader.split(',').map((value) => value.trim())
+ const filteredValues = values.filter(
+ (value) => value !== 'msw/passthrough',
+ )
+
+ if (filteredValues.length > 0) {
+ headers.set('accept', filteredValues.join(', '))
+ } else {
+ headers.delete('accept')
+ }
+ }
+
+ return fetch(requestClone, { headers })
+ }
+
+ // Bypass mocking when the client is not active.
+ if (!client) {
+ return passthrough()
+ }
+
+ // Bypass initial page load requests (i.e. static assets).
+ // The absence of the immediate/parent client in the map of the active clients
+ // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
+ // and is not ready to handle requests.
+ if (!activeClientIds.has(client.id)) {
+ return passthrough()
+ }
+
+ // Notify the client that a request has been intercepted.
+ const serializedRequest = await serializeRequest(event.request)
+ const clientMessage = await sendToClient(
+ client,
+ {
+ type: 'REQUEST',
+ payload: {
+ id: requestId,
+ interceptedAt: requestInterceptedAt,
+ ...serializedRequest,
+ },
+ },
+ [serializedRequest.body],
+ )
+
+ switch (clientMessage.type) {
+ case 'MOCK_RESPONSE': {
+ return respondWithMock(clientMessage.data)
+ }
+
+ case 'PASSTHROUGH': {
+ return passthrough()
+ }
+ }
+
+ return passthrough()
+}
+
+/**
+ * @param {Client} client
+ * @param {any} message
+ * @param {Array} transferrables
+ * @returns {Promise}
+ */
+function sendToClient(client, message, transferrables = []) {
+ return new Promise((resolve, reject) => {
+ const channel = new MessageChannel()
+
+ channel.port1.onmessage = (event) => {
+ if (event.data && event.data.error) {
+ return reject(event.data.error)
+ }
+
+ resolve(event.data)
+ }
+
+ client.postMessage(message, [
+ channel.port2,
+ ...transferrables.filter(Boolean),
+ ])
+ })
+}
+
+/**
+ * @param {Response} response
+ * @returns {Response}
+ */
+function respondWithMock(response) {
+ // Setting response status code to 0 is a no-op.
+ // However, when responding with a "Response.error()", the produced Response
+ // instance will have status code set to 0. Since it's not possible to create
+ // a Response instance with status code 0, handle that use-case separately.
+ if (response.status === 0) {
+ return Response.error()
+ }
+
+ const mockedResponse = new Response(response.body, response)
+
+ Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
+ value: true,
+ enumerable: true,
+ })
+
+ return mockedResponse
+}
+
+/**
+ * @param {Request} request
+ */
+async function serializeRequest(request) {
+ return {
+ url: request.url,
+ mode: request.mode,
+ method: request.method,
+ headers: Object.fromEntries(request.headers.entries()),
+ cache: request.cache,
+ credentials: request.credentials,
+ destination: request.destination,
+ integrity: request.integrity,
+ redirect: request.redirect,
+ referrer: request.referrer,
+ referrerPolicy: request.referrerPolicy,
+ body: await request.arrayBuffer(),
+ keepalive: request.keepalive,
+ }
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000000..1a69d6bc41
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/CommandIcon.svg b/frontend/src/assets/CommandIcon.svg
new file mode 100644
index 0000000000..ba45dd8215
--- /dev/null
+++ b/frontend/src/assets/CommandIcon.svg
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/frontend/src/assets/NoSaga.svg b/frontend/src/assets/NoSaga.svg
new file mode 100644
index 0000000000..7126d918b1
--- /dev/null
+++ b/frontend/src/assets/NoSaga.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/SagaCompletedIcon.svg b/frontend/src/assets/SagaCompletedIcon.svg
new file mode 100644
index 0000000000..edf6156c9c
--- /dev/null
+++ b/frontend/src/assets/SagaCompletedIcon.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/src/assets/SagaIcon.svg b/frontend/src/assets/SagaIcon.svg
new file mode 100644
index 0000000000..c37953fb4a
--- /dev/null
+++ b/frontend/src/assets/SagaIcon.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/frontend/src/assets/SagaInitiatedIcon.svg b/frontend/src/assets/SagaInitiatedIcon.svg
new file mode 100644
index 0000000000..da9691b48f
--- /dev/null
+++ b/frontend/src/assets/SagaInitiatedIcon.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/frontend/src/assets/SagaTimeoutIcon.svg b/frontend/src/assets/SagaTimeoutIcon.svg
new file mode 100644
index 0000000000..31eb4e0a8e
--- /dev/null
+++ b/frontend/src/assets/SagaTimeoutIcon.svg
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/frontend/src/assets/SagaUpdatedIcon.svg b/frontend/src/assets/SagaUpdatedIcon.svg
new file mode 100644
index 0000000000..04c8789377
--- /dev/null
+++ b/frontend/src/assets/SagaUpdatedIcon.svg
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/Shell_CopyClipboard.svg b/frontend/src/assets/Shell_CopyClipboard.svg
new file mode 100644
index 0000000000..cdc8ef6e39
--- /dev/null
+++ b/frontend/src/assets/Shell_CopyClipboard.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/Shell_ToolbarEndpoint.svg b/frontend/src/assets/Shell_ToolbarEndpoint.svg
new file mode 100644
index 0000000000..23c5003fce
--- /dev/null
+++ b/frontend/src/assets/Shell_ToolbarEndpoint.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/TimeoutIcon.svg b/frontend/src/assets/TimeoutIcon.svg
new file mode 100644
index 0000000000..539c2a0ccf
--- /dev/null
+++ b/frontend/src/assets/TimeoutIcon.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/command.svg b/frontend/src/assets/command.svg
new file mode 100644
index 0000000000..9f39793085
--- /dev/null
+++ b/frontend/src/assets/command.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/diff-close.svg b/frontend/src/assets/diff-close.svg
new file mode 100644
index 0000000000..b93e269dd8
--- /dev/null
+++ b/frontend/src/assets/diff-close.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/diff-maximize.svg b/frontend/src/assets/diff-maximize.svg
new file mode 100644
index 0000000000..72b9dbc891
--- /dev/null
+++ b/frontend/src/assets/diff-maximize.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/dropdown.css b/frontend/src/assets/dropdown.css
new file mode 100644
index 0000000000..547ee0aca4
--- /dev/null
+++ b/frontend/src/assets/dropdown.css
@@ -0,0 +1,78 @@
+.dropdown-menu {
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 100;
+ text-align: left;
+ list-style: none;
+}
+
+.dropdown-menu > span > li > button {
+ border: none;
+ background: none;
+}
+
+.dropdown-menu > span > li > button,
+.dropdown-menu > li > button,
+.dropdown-menu > li > a,
+.dropdown-menu > span > li > a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: 400;
+ line-height: 1.42857143;
+ color: #333333;
+ white-space: nowrap;
+}
+
+ul.dropdown-menu > span > li > a > span {
+ color: #aaa;
+}
+
+.dropdown-menu > span > li > a:hover,
+.dropdown-menu > span > li > a:focus,
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus,
+.dropdown-menu > span > li > button:hover,
+.dropdown-menu > span > li > button:focus,
+.dropdown-menu > li > button:hover,
+.dropdown-menu > li > button:focus {
+ color: #262626;
+ text-decoration: none;
+ background-color: #f5f5f5;
+}
+
+.msg-list-dropdown {
+ margin: 1px 0 0 0 !important;
+ padding-right: 0;
+}
+
+.msg-group-menu {
+ margin: 21px 0px 0 6px;
+ float: right;
+ padding-top: 12px;
+}
+
+.msg-group-menu > .control-label {
+ float: none;
+}
+
+.btn.sp-btn-menu {
+ padding-left: 16px;
+ background: none;
+ border: none;
+ color: var(--sp-blue);
+ padding-right: 0;
+}
+
+.sp-btn-menu:hover {
+ background: none;
+ border: none;
+ color: var(--sp-blue);
+ text-decoration: underline;
+}
+
+.btn-toolbar > .btn-default:hover {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #adadad;
+}
diff --git a/frontend/src/assets/endpoint-lost.svg b/frontend/src/assets/endpoint-lost.svg
new file mode 100644
index 0000000000..a6c73ab95b
--- /dev/null
+++ b/frontend/src/assets/endpoint-lost.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/endpoint.svg b/frontend/src/assets/endpoint.svg
new file mode 100644
index 0000000000..6e23ac345a
--- /dev/null
+++ b/frontend/src/assets/endpoint.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/assets/event.svg b/frontend/src/assets/event.svg
new file mode 100644
index 0000000000..c4222f78c9
--- /dev/null
+++ b/frontend/src/assets/event.svg
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/failed-msg.svg b/frontend/src/assets/failed-msg.svg
new file mode 100644
index 0000000000..2f581c9d49
--- /dev/null
+++ b/frontend/src/assets/failed-msg.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/frontend/src/assets/footer.css b/frontend/src/assets/footer.css
new file mode 100644
index 0000000000..e52111e454
--- /dev/null
+++ b/frontend/src/assets/footer.css
@@ -0,0 +1,107 @@
+footer {
+ color: #bcc6c2;
+ font-size: 14px;
+ font-weight: normal;
+ margin-top: 100px;
+ text-align: left;
+ text-align: center;
+ width: 100%;
+ background-color: #fff;
+ position: fixed;
+ bottom: 0;
+ border-top: 1px solid #ddd;
+ box-shadow: 0px -10px 16px #f2f6f7;
+ padding-top: 7px;
+ z-index: 999; /* this needs to fight with vue-highlight-code which forces z-index to 700 */
+}
+
+footer .row {
+ display: flex;
+ justify-content: space-between;
+}
+
+footer span {
+ padding-left: 0;
+ padding-right: 32px;
+}
+
+footer span.connected-status {
+ color: #000;
+ padding-right: 0;
+}
+
+footer span.newscversion {
+ padding-right: 0;
+}
+
+.versionnumber,
+footer span.monitoring-connected,
+footer span.monitoring-connected span {
+ padding-right: 0;
+}
+
+.footer .container {
+ width: 100%;
+}
+
+footer span {
+ color: #777f7f;
+ display: inline-block;
+ padding: 0 8px;
+}
+
+footer a {
+ font-weight: normal;
+}
+
+footer a:hover {
+ font-weight: normal;
+}
+
+@media (max-width: 1550px) {
+ footer {
+ font-size: 12px;
+ }
+}
+
+.connectivity-status div,
+.connectivity-status span,
+.connectivity-status span span {
+ display: inline-block;
+ color: #777f7f;
+}
+
+.connectivity-status i,
+.connectivity-status div.pa-connection-success,
+.connectivity-status div.pa-connection-failed {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+}
+
+.connectivity-status .connection-failed {
+ color: #ce4844;
+}
+
+.connectivity-status .pa-connection-failed {
+ background: #ce4844;
+}
+
+.connectivity-status .pa-connection-success {
+ background: #00c468;
+}
+
+.connectivity-status .secondary {
+ display: none;
+}
+
+@media (min-width: 960px) {
+ .connectivity-status .secondary {
+ display: inline-block;
+ }
+}
+
+.trialLicenseBar {
+ background-color: firebrick;
+ color: white;
+}
diff --git a/frontend/src/assets/header-menu-item.css b/frontend/src/assets/header-menu-item.css
new file mode 100644
index 0000000000..01da32a49c
--- /dev/null
+++ b/frontend/src/assets/header-menu-item.css
@@ -0,0 +1,13 @@
+.nav {
+ --bs-link-color: #9d9d9d;
+ --bs-link-hover-color: #fff;
+}
+
+.navbar > .container-fluid > div {
+ margin: 0 1em;
+}
+
+#navbar .router-link-active {
+ background: transparent !important;
+ border-bottom: 5px solid var(--sp-blue);
+}
diff --git a/frontend/src/assets/loader_spinner.gif b/frontend/src/assets/loader_spinner.gif
new file mode 100644
index 0000000000..337ea0dcf4
Binary files /dev/null and b/frontend/src/assets/loader_spinner.gif differ
diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg
new file mode 100644
index 0000000000..035f766bbb
--- /dev/null
+++ b/frontend/src/assets/logo.svg
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css
new file mode 100644
index 0000000000..a3bd8e2a06
--- /dev/null
+++ b/frontend/src/assets/main.css
@@ -0,0 +1,510 @@
+@import "bootstrap/dist/css/bootstrap.css";
+@import "footer.css";
+@import "tabs.css";
+@import "dropdown.css";
+@import "toast.css";
+@import "pagination.css";
+
+:root {
+ --bs-btn-hover-color: #333;
+ --bs-btn-hover-bg: #e6e6e6;
+ --bs-btn-hover-border-color: #adadad;
+ --monitoring-queue-length: #ea7e00;
+ --monitoring-queue-length-light: #eaddce;
+ --monitoring-throughput: #176397;
+ --monitoring-throughput-light: #cadce8;
+ --monitoring-retries: #cc1252;
+ --monitoring-retries-light: #e9c4d1;
+ --monitoring-processing-time: #258135;
+ --monitoring-processing-time-light: #bee6c5;
+ --monitoring-critical-time: #2700cb;
+ --monitoring-critical-time-light: #c4bce5;
+ --sp-blue: #00a3c4;
+ --info-icon: #0d6efd;
+ --reduced-emphasis: #929e9e;
+ /* FA7 introduced fixed width icons, whereas our design was based on auto width. Forcing it back to fix alignment issues*/
+ --fa-width: auto;
+}
+
+body {
+ padding-top: 0px;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857143;
+ background-color: #f2f6f7 !important;
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ min-width: 930px;
+}
+
+.fa {
+ color: #929e9e;
+}
+
+.badge.badge-important {
+ background-color: #ce4844;
+ background-image: none;
+ border: none;
+}
+
+.badge {
+ display: inline-block;
+ min-width: 10px;
+ padding: 3px 7px;
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: middle;
+ background-color: #777777;
+ border-radius: 10px;
+}
+
+.container {
+ width: 92%;
+ max-width: inherit;
+ padding: 0;
+}
+
+.container-fluid {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+#main-content {
+ padding-top: 40px;
+ padding-bottom: 40px;
+ overflow: auto;
+ flex: 1;
+}
+
+h1 {
+ font-size: 30px;
+ font-weight: 700;
+ margin: 0 0 32px;
+}
+
+h3 {
+ font-size: 20px;
+ font-weight: 700;
+}
+
+h4 {
+ font-size: 18px;
+}
+
+h5 {
+ color: #929e9e;
+ display: inline-block !important;
+ font-size: 14px;
+ font-weight: bold;
+ margin: 0 0 14px;
+ margin-right: 30px;
+ text-transform: uppercase;
+}
+
+h6 {
+ font-size: 18px;
+ font-weight: 700;
+ color: #181919;
+}
+
+h6 a:hover {
+ cursor: pointer;
+}
+
+a {
+ text-decoration: none;
+ color: var(--sp-blue);
+ outline: none;
+ border: none;
+}
+
+a:hover {
+ color: var(--sp-blue);
+ text-decoration: underline;
+}
+
+body {
+ padding-top: 0px;
+}
+
+span.connection-successful,
+span.connection-successful i {
+ color: #00c468 !important;
+}
+
+span.connection-failed,
+span.connection-failed i,
+.failed-validation,
+.failed-validation i {
+ color: #ce4844 !important;
+}
+
+.failed-validation {
+ margin-left: 5px;
+}
+span.connection-test {
+ position: relative;
+ top: 14px;
+ left: 10px;
+ text-transform: uppercase;
+ font-weight: bold;
+ color: black;
+}
+
+form .connection h3 {
+ margin-bottom: 16px;
+}
+
+form .connection .form-group {
+ padding-left: 0;
+}
+
+.connection:nth-child(2) h3 {
+ margin-top: 40px;
+}
+
+form .connection .form-group input {
+ font-size: 16px;
+ height: 44px;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+label {
+ display: inline-block;
+ max-width: 100%;
+ margin-bottom: 5px;
+ font-weight: 700;
+}
+
+.label {
+ display: inline;
+ padding: 0.2em 0.6em 0.3em;
+ font-size: 75%;
+ font-weight: 700;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25em;
+}
+
+.label-info,
+.badge-info {
+ background-color: #1b809e;
+ border-color: #1b809e;
+}
+
+.label-warning,
+.badge-warning {
+ background-color: #aa6708;
+ border-color: #aa6708;
+}
+
+.label-important,
+.badge-important {
+ background-color: #fa603d;
+ border-color: #fa5833;
+}
+
+pre {
+ display: block;
+ padding: 9.5px;
+ margin: 0 0 10px;
+ font-size: 13px;
+ line-height: 1.42857143;
+ color: #333333;
+ word-break: break-all;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.has-error .form-control {
+ border-color: #a94442;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px #00000013;
+}
+
+.form-control {
+ width: 100% !important;
+ font-size: 14px;
+}
+
+.btn-default {
+ padding: 8px 16px;
+ color: #333;
+ background-color: #fff;
+ border-color: #ccc;
+}
+
+.btn-default:hover {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #adadad;
+}
+
+.btn.btn-default.disabled {
+ opacity: 0.4;
+}
+
+.btn.btn-default.disabled:hover {
+ border-color: var(--sp-blue);
+}
+
+.btn.btn-primary {
+ padding: 8px 16px;
+ background-color: var(--sp-blue);
+ border-color: #0686aa;
+ color: #fff;
+}
+
+.btn-primary:hover {
+ background-color: #0db3c8;
+ border-color: var(--sp-blue);
+ color: #fff;
+}
+
+.btn-primary i {
+ color: #ffffff;
+ font-size: 12px;
+}
+
+.btn-secondary {
+ background-color: #fff;
+ color: var(--sp-blue);
+ border-color: var(--sp-blue);
+}
+
+.btn-secondary:hover {
+ color: var(--sp-blue);
+ border-color: var(--sp-blue);
+ background-color: #fff;
+ opacity: 0.7;
+}
+
+.btn-secondary.disabled {
+ border-color: var(--sp-blue);
+}
+
+.row {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.danger {
+ color: #ce4844 !important;
+ font-weight: bold !important;
+}
+
+.btn-link:hover,
+.btn-link:focus {
+ color: #23527c;
+ text-decoration: underline;
+ background-color: transparent;
+}
+
+input.check-label {
+ margin: 4px 6px 0 0;
+ float: left;
+}
+
+.btn {
+ font-size: 14px;
+}
+
+.has-error .help-block,
+.has-error .control-label,
+.has-error .radio,
+.has-error .checkbox,
+.has-error .radio-inline,
+.has-error .checkbox-inline,
+.has-error.radio label,
+.has-error.checkbox label,
+.has-error.radio-inline label,
+.has-error.checkbox-inline label {
+ color: #a94442;
+}
+
+.control-label {
+ float: left;
+ padding-top: 7.5px;
+ margin-right: -10px;
+}
+
+.btn-toolbar {
+ padding: 12px 0 0;
+ margin-left: 0;
+}
+
+.btn-toolbar > .btn,
+.btn-toolbar > .btn-group,
+.btn-toolbar > .input-group,
+.action-btns .btn {
+ margin-left: 0;
+ margin-right: 5px;
+}
+
+.metadata .label-important {
+ border-radius: 3px;
+ color: white;
+ font-size: 13px;
+ font-weight: bold;
+ margin-right: 20px;
+}
+
+.metadata > .btn-sm > i {
+ color: var(--sp-blue);
+}
+
+.metadata:first-child {
+ padding-left: 0;
+}
+
+span.metadata {
+ display: inline-block;
+ padding: 0px 20px 2px 0;
+ color: #777f7f;
+}
+
+span.metadata.metadata-link i.fa {
+ color: var(--sp-blue);
+}
+
+.metadata > .in-progress {
+ font-style: italic;
+ color: white;
+}
+
+.metadata-label {
+ margin-right: 24px;
+ position: relative;
+ top: -1px;
+}
+
+.metadata > .metadata-label {
+ padding: 6px 10px;
+}
+
+.btn-xs,
+.btn-group-xs > .btn {
+ border-radius: 3px;
+}
+
+.btn-retry-dismiss {
+ position: relative;
+ height: 28px;
+ width: 74px;
+ top: -2px;
+ left: 11px;
+ line-height: 1;
+}
+
+.btn.btn-retry-dismiss {
+ border-radius: 4px;
+ padding: 8px 16px;
+}
+
+.btn.btn-xs {
+ font-size: 12px;
+}
+
+.btn.disabled,
+.btn[disabled],
+fieldset[disabled] {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #adadad;
+ pointer-events: all;
+}
+
+.btn.disabled:hover,
+.btn[disabled]:hover,
+fieldset[disabled]:hover {
+ cursor: not-allowed;
+}
+.box-group:hover {
+ background-color: #edf6f7;
+ border: 1px solid var(--sp-blue);
+ cursor: pointer;
+}
+
+.box-group {
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid #eee;
+ padding: 15px 40px;
+}
+
+.box-container {
+ padding: 0;
+ background-color: #fff;
+}
+
+.box-container .list-section .col-12.form-group {
+ margin-bottom: 0;
+}
+
+.select-all {
+ width: 127px;
+}
+
+.group-message-count {
+ color: #a8b3b1;
+ font-size: 16px;
+ margin: 4px 0 12px;
+ display: block;
+}
+
+h3.group-message-count {
+ color: #a8b3b1;
+ font-size: 16px;
+ margin: 4px 0 12px;
+ display: block;
+}
+
+.group-title {
+ display: block;
+ font-size: 30px;
+ margin: 10px 0 0;
+}
+
+h2.group-title,
+h3.group-title {
+ font-weight: bold;
+ line-height: 28px;
+}
+
+div.alert.alert-warning strong {
+ text-transform: uppercase;
+}
+
+.sticky-warning.alert.alert-warning,
+.alert.alert-warning,
+.alert.alert-danger {
+ font-size: 16px;
+ margin: 25px 0 0;
+ height: unset;
+}
+
+/* TODO: why do these have to be !important? */
+.fake-link {
+ color: var(--sp-blue) !important;
+ text-decoration: none !important;
+}
+
+.tooltip {
+ font-size: 12px;
+}
diff --git a/frontend/src/assets/monitoring-lost.svg b/frontend/src/assets/monitoring-lost.svg
new file mode 100644
index 0000000000..5496b71809
--- /dev/null
+++ b/frontend/src/assets/monitoring-lost.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/monitoring.svg b/frontend/src/assets/monitoring.svg
new file mode 100644
index 0000000000..94ed3cabef
--- /dev/null
+++ b/frontend/src/assets/monitoring.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/navbar.css b/frontend/src/assets/navbar.css
new file mode 100644
index 0000000000..4f44cf7d0b
--- /dev/null
+++ b/frontend/src/assets/navbar.css
@@ -0,0 +1,235 @@
+:root {
+ --bs-link-color: #9d9d9d;
+ --bs-link-hover-color: #fff;
+}
+
+.navbar {
+ height: 60px;
+ flex-wrap: nowrap;
+}
+
+.navbar-inverse {
+ background-color: #1a1a1a;
+}
+
+.navbar > .navbar-right {
+ margin-right: -15px;
+}
+
+.navbar-brand {
+ height: 60px;
+ padding-bottom: 9px;
+ padding-top: 10px;
+}
+
+.navbar-brand img {
+ width: 160px;
+ margin-left: 3px;
+}
+
+.navbar-nav > li > a > span {
+ margin-left: 8px;
+}
+
+.nav > li > a {
+ position: relative;
+ display: block;
+ padding: 10px 15px;
+ color: var(--bs-link-color);
+ text-decoration: none;
+}
+
+.nav > li > a:hover,
+.nav > li > a:focus {
+ color: #fff;
+}
+
+.navbar-nav > li > a > span.no-margin {
+ margin: 0;
+}
+
+.nav > li > a .navbar-toggle {
+ margin-top: 13px;
+}
+
+.navbar a {
+ font-weight: normal;
+ text-decoration: none;
+}
+
+.navbar a:hover {
+ font-weight: normal;
+}
+
+.navbar-inverse .navbar-nav > .active > a,
+.navbar-inverse .navbar-nav > .active > a:hover,
+.navbar-inverse .navbar-nav > .active > a:focus {
+ color: #fff;
+}
+
+.label-important,
+.badge-important {
+ background-color: #fa603d;
+ border-color: #fa5833;
+}
+
+@media (min-width: 768px) {
+ .navbar-nav > li.active > a {
+ background: transparent !important;
+ border-bottom: 5px solid var(--sp-blue);
+ }
+
+ .navbar-nav > li > a {
+ padding-bottom: 15px;
+ padding-top: 20px;
+ }
+
+ .graph-values .col-sm-6 {
+ width: 45%;
+ }
+}
+
+@media only screen and (min-width: 1072px) {
+ .navbar-label {
+ display: inline;
+ }
+}
+
+@media only screen and (max-width: 768px) {
+ .navbar-collapse.collapse.in {
+ padding: 0 0 0 16px !important;
+ }
+}
+
+@media (max-width: 1199px) {
+ .navbar-header {
+ float: none;
+ }
+
+ .navbar-toggle {
+ display: block;
+ }
+
+ .navbar-collapse {
+ border-top: 1px solid transparent;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ }
+
+ .navbar-collapse.collapse {
+ display: none !important;
+ }
+
+ .navbar-nav {
+ float: none !important;
+ margin: 7.5px -15px;
+ }
+
+ .navbar-nav > li {
+ float: none;
+ }
+
+ .navbar-nav > li > a {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ }
+
+ .navbar-text {
+ float: none;
+ margin: 15px 0;
+ }
+
+ .navbar-collapse.collapse.in {
+ display: block !important;
+ padding: 0 32px 16px 32px;
+ }
+
+ .collapsing {
+ overflow: hidden !important;
+ }
+}
+
+@media (max-width: 1450px) {
+ .navbar-label {
+ display: none;
+ }
+
+ .nav > li > a .icon-white {
+ color: #929e9e;
+ }
+
+ .nav > li > a:hover .icon-white,
+ .nav > li > a:focus .icon-white {
+ color: #fff;
+ }
+
+ .nav > li > a:hover .pa-monitoring,
+ .nav > li > a:focus .pa-monitoring {
+ background-color: #fff;
+ }
+}
+
+@media (max-width: 1439px) {
+ nav.navbar {
+ position: sticky;
+ }
+
+ div.navbar-header {
+ float: left;
+ }
+
+ ul.navbar-nav {
+ margin: 0 8px 0 0;
+ display: block;
+ }
+
+ .navbar-nav > li > a {
+ padding-top: 18px;
+ padding-bottom: 17px;
+ }
+
+ .navbar-collapse.collapse {
+ display: block !important;
+ }
+
+ .navbar-nav > li,
+ .navbar-nav {
+ float: left !important;
+ height: 59px;
+ }
+
+ .navbar-nav.navbar-right:last-child {
+ margin-right: -15px !important;
+ }
+
+ .navbar-right {
+ float: right !important;
+ }
+
+ .navbar-nav > li.active > a {
+ background: transparent !important;
+ border-bottom: 5px solid var(--sp-blue) !important;
+ }
+
+ .tabs h5 {
+ margin-bottom: 0;
+ padding-bottom: 10px;
+ }
+}
+
+@media (max-width: 1439px) and (-ms-high-contrast: active), (-ms-high-contrast: none) {
+ nav.navbar {
+ margin-top: 0;
+ }
+}
+
+/* TODO move this to the page/control that it's used in*/
+.pa-monitoring {
+ mask-image: url("@/assets/monitoring.svg");
+ background-color: #929e9e;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 16px;
+ height: 14px;
+ position: relative;
+ top: 2px;
+}
diff --git a/frontend/src/assets/pagination.css b/frontend/src/assets/pagination.css
new file mode 100644
index 0000000000..9c289e5892
--- /dev/null
+++ b/frontend/src/assets/pagination.css
@@ -0,0 +1,40 @@
+.pagination button.active,
+.pagination .active > span,
+.pagination button.active:hover,
+.pagination .active > span:hover,
+.pagination button.active:focus,
+.pagination .active > span:focus {
+ background-color: var(--sp-blue);
+ border-color: #ddd;
+ color: var(--bs-pagination-active-color);
+ outline: none;
+ box-shadow: none;
+}
+
+.pagination {
+ --bs-link-color: #00729c;
+ --bs-pagination-font-size: 14px;
+}
+
+.pagination {
+ margin: 1.5em;
+}
+
+.pagination > li > button:hover,
+.pagination > li > span:hover,
+.pagination > li > button:focus,
+.pagination > li > span:focus {
+ z-index: 2;
+ color: #23527c;
+ background-color: #eeeeee;
+ border-color: #ddd;
+ outline: none;
+ box-shadow: none;
+}
+
+.pagination .dropdown button:hover {
+ background: none;
+ border: none;
+ color: var(--sp-blue);
+ text-decoration: underline;
+}
diff --git a/frontend/src/assets/redirect-destination.svg b/frontend/src/assets/redirect-destination.svg
new file mode 100644
index 0000000000..50ee436fd9
--- /dev/null
+++ b/frontend/src/assets/redirect-destination.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/redirect-source.svg b/frontend/src/assets/redirect-source.svg
new file mode 100644
index 0000000000..2b7f20d704
--- /dev/null
+++ b/frontend/src/assets/redirect-source.svg
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/saga-completed.svg b/frontend/src/assets/saga-completed.svg
new file mode 100644
index 0000000000..e63f5a58e6
--- /dev/null
+++ b/frontend/src/assets/saga-completed.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/frontend/src/assets/saga-initiated.svg b/frontend/src/assets/saga-initiated.svg
new file mode 100644
index 0000000000..969373a9cd
--- /dev/null
+++ b/frontend/src/assets/saga-initiated.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/frontend/src/assets/saga-trigger.svg b/frontend/src/assets/saga-trigger.svg
new file mode 100644
index 0000000000..0b809af350
--- /dev/null
+++ b/frontend/src/assets/saga-trigger.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/saga.svg b/frontend/src/assets/saga.svg
new file mode 100644
index 0000000000..f284fcc527
--- /dev/null
+++ b/frontend/src/assets/saga.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/si-icon.svg b/frontend/src/assets/si-icon.svg
new file mode 100644
index 0000000000..4b7e4561fc
--- /dev/null
+++ b/frontend/src/assets/si-icon.svg
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/sort-down.svg b/frontend/src/assets/sort-down.svg
new file mode 100644
index 0000000000..75bcf850ea
--- /dev/null
+++ b/frontend/src/assets/sort-down.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/sort-up.svg b/frontend/src/assets/sort-up.svg
new file mode 100644
index 0000000000..91312998d8
--- /dev/null
+++ b/frontend/src/assets/sort-up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/sp-loader.gif b/frontend/src/assets/sp-loader.gif
new file mode 100644
index 0000000000..0054645e2b
Binary files /dev/null and b/frontend/src/assets/sp-loader.gif differ
diff --git a/frontend/src/assets/status_archived.svg b/frontend/src/assets/status_archived.svg
new file mode 100644
index 0000000000..3ee4db5938
--- /dev/null
+++ b/frontend/src/assets/status_archived.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/status_failed.svg b/frontend/src/assets/status_failed.svg
new file mode 100644
index 0000000000..ff5a79e20f
--- /dev/null
+++ b/frontend/src/assets/status_failed.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/frontend/src/assets/status_repeated_failed.svg b/frontend/src/assets/status_repeated_failed.svg
new file mode 100644
index 0000000000..c80647d759
--- /dev/null
+++ b/frontend/src/assets/status_repeated_failed.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/status_resolved.svg b/frontend/src/assets/status_resolved.svg
new file mode 100644
index 0000000000..b7ab8c1f60
--- /dev/null
+++ b/frontend/src/assets/status_resolved.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/frontend/src/assets/status_retry_issued.svg b/frontend/src/assets/status_retry_issued.svg
new file mode 100644
index 0000000000..f64c7dc974
--- /dev/null
+++ b/frontend/src/assets/status_retry_issued.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/frontend/src/assets/status_successful.svg b/frontend/src/assets/status_successful.svg
new file mode 100644
index 0000000000..340e02ac40
--- /dev/null
+++ b/frontend/src/assets/status_successful.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/frontend/src/assets/tabs.css b/frontend/src/assets/tabs.css
new file mode 100644
index 0000000000..489ab7f79b
--- /dev/null
+++ b/frontend/src/assets/tabs.css
@@ -0,0 +1,104 @@
+.nav-item.active {
+ border-bottom: 3px solid #181919;
+ color: #181919;
+}
+
+.nav-item {
+ padding-bottom: 10px;
+ margin-bottom: 0;
+}
+
+.nav-item a {
+ color: var(--sp-blue);
+ cursor: pointer;
+ font-size: 16px;
+}
+
+h5.nav-item {
+ color: #929e9e;
+ display: inline-block !important;
+ font-size: 14px;
+ font-weight: 700;
+ margin: 0 30px 14px 0;
+ margin-bottom: 0;
+ text-transform: uppercase;
+}
+
+.box {
+ box-shadow: none;
+ margin: 0;
+ padding-bottom: 10px;
+ background-color: #fff;
+ border-top: 1px solid #eee;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #eee;
+ border-left: 1px solid #fff;
+ margin: 0.5rem 0 1rem;
+ margin-bottom: 5px;
+ padding: 20px;
+}
+
+.box-no-click {
+ background: none;
+ border-bottom: 1px solid #ced6d3;
+ border-left: none;
+ border-right: none;
+ cursor: default;
+}
+
+.no-side-padding {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.tabs {
+ border-bottom: 1px solid #e6e6e6;
+ padding: 0;
+ margin-bottom: -1px;
+}
+
+.tabs h5 {
+ display: inline;
+ padding-bottom: 10px;
+ margin-bottom: 0;
+}
+
+.tabs h5 a:hover {
+ text-decoration: none;
+}
+
+.tabs h5.active {
+ border-bottom: 3px solid #181919;
+ color: #181919;
+}
+
+.tabs h5.active a {
+ color: #181919;
+ text-decoration: none;
+}
+
+.tabs a {
+ color: var(--sp-blue);
+ cursor: pointer;
+ font-size: 16px;
+}
+
+.tabs a:hover {
+ color: #00729c;
+}
+
+.tabs h5.active > a:hover {
+ color: #181919;
+ cursor: default;
+ text-decoration: none;
+}
+
+.tabs h5.disabled > a {
+ color: #aaa;
+ cursor: default;
+ text-decoration: none;
+}
+
+.tabs h5.disabled > a:hover {
+ cursor: not-allowed;
+}
diff --git a/frontend/src/assets/timeout.svg b/frontend/src/assets/timeout.svg
new file mode 100644
index 0000000000..40869a1988
--- /dev/null
+++ b/frontend/src/assets/timeout.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/assets/toast-danger.svg b/frontend/src/assets/toast-danger.svg
new file mode 100644
index 0000000000..d9e1ef7722
--- /dev/null
+++ b/frontend/src/assets/toast-danger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/toast-warning.svg b/frontend/src/assets/toast-warning.svg
new file mode 100644
index 0000000000..23ea51af32
--- /dev/null
+++ b/frontend/src/assets/toast-warning.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/assets/toast.css b/frontend/src/assets/toast.css
new file mode 100644
index 0000000000..c269498b4a
--- /dev/null
+++ b/frontend/src/assets/toast.css
@@ -0,0 +1,188 @@
+/* Vue-Toastification Globals */
+.Vue-Toastification__toast {
+ border-radius: 5px;
+ width: 100%;
+ height: 100%;
+ padding: 12px 12px 24px 15px;
+ color: white;
+}
+
+.Vue-Toastification__toast-component-body {
+ flex: 1;
+ margin-top: 15px;
+}
+
+.Vue-Toastification__toast p {
+ margin-bottom: 0;
+}
+
+.Vue-Toastification__toast--default.vue-toast {
+ background-color: #030303;
+}
+.Vue-Toastification__toast--success.vue-toast {
+ background-color: #00c468;
+}
+.Vue-Toastification__toast--error.vue-toast {
+ background-color: #ce4844;
+}
+.Vue-Toastification__toast--info.vue-toast {
+ background-color: #1b809e;
+}
+.Vue-Toastification__toast--warning.vue-toast {
+ background-color: #f3bc52;
+}
+
+.Vue-Toastification__fade-leave-active {
+ height: auto !important;
+}
+
+.Vue-Toastification__icon {
+ vertical-align: top;
+ align-items: top;
+ margin: 18px 10px auto 0px;
+ color: white;
+}
+
+.toast-close-button {
+ vertical-align: top;
+ height: 100%;
+ padding-left: 0;
+ color: white;
+}
+
+@keyframes fadeInRight {
+ 0% {
+ opacity: 0;
+ transform: translateX(20px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateX(0);
+ }
+}
+
+.toast-container {
+ width: 400px;
+ padding-bottom: 24px;
+ padding-right: 0px;
+ position: fixed;
+}
+
+.toast-container > :not(:last-child) {
+ margin-bottom: 1rem;
+}
+
+.toast-message a.btn:hover {
+ opacity: 0.85;
+ text-decoration: none;
+}
+
+.toast-message a {
+ color: #000080;
+}
+
+.toast-message a:hover {
+ color: #000080;
+ text-decoration: underline;
+}
+
+.toast-message a.btn.btn-light {
+ margin-top: 16px;
+ margin-left: 10px;
+ color: #000;
+ border: 1px solid #000;
+ background-color: rgba(0, 0, 0, 0);
+}
+
+.toast-message a.btn.btn-light:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ text-decoration: none;
+}
+
+/* Warning Toast */
+.toast-message a.btn.btn-warning {
+ margin-top: 16px;
+ color: #f3bc52;
+ border: 1px solid #000;
+ background-color: rgba(0, 0, 0, 1);
+}
+
+/* Error Toast */
+.toast-message a.btn.btn-error {
+ margin-top: 16px;
+ color: #ce4844;
+ border: 1px solid #000;
+ background-color: rgba(0, 0, 0, 1);
+}
+
+.toast-error a.btn.btn-default {
+ color: #bd362f;
+ padding-right: 20px;
+ padding-left: 20px;
+ border: none;
+ margin-top: 16px;
+}
+
+/* Info Toast */
+.toast-message a.btn.btn-info {
+ margin-top: 16px;
+ color: #1b809e;
+ border: 1px solid #000;
+ background-color: rgba(0, 0, 0, 1);
+}
+
+/* Success Toast */
+.toast-message a.btn.btn-success {
+ margin-top: 16px;
+ color: #00c468;
+ border: 1px solid #000;
+ background-color: rgba(0, 0, 0, 1);
+}
+
+/* Default Toast */
+.Vue-Toastification__toast--default {
+ color: #fff;
+}
+
+.toast-default a.btn.btn-default {
+ margin-top: 16px;
+ color: #000;
+ border: 1px solid #fff;
+ background-color: rgba(255, 255, 255, 1);
+}
+
+.toast-default a.btn.btn-light {
+ margin-top: 16px;
+ margin-left: 10px;
+ color: #fff;
+ border: 1px solid #fff;
+ background-color: rgba(255, 255, 255, 0);
+}
+
+.toast-default a.btn.btn-light:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+ text-decoration: none;
+}
+
+.toast-default a {
+ color: var(--sp-blue);
+}
+
+.toast-default a:hover {
+ color: var(--sp-blue);
+ text-decoration: underline;
+}
+
+.Vue-Toastification__toast--default .toast-close-button {
+ color: #fff;
+ opacity: 0.5;
+}
+
+.Vue-Toastification__toast--default .toast-close-button:hover {
+ color: #fff;
+ opacity: 0.8;
+}
+
+.Vue-Toastification__toast--default .Vue-Toastification__icon {
+ color: #fff;
+}
diff --git a/frontend/src/assets/warning.svg b/frontend/src/assets/warning.svg
new file mode 100644
index 0000000000..48952aa411
--- /dev/null
+++ b/frontend/src/assets/warning.svg
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ActionButton.vue b/frontend/src/components/ActionButton.vue
new file mode 100644
index 0000000000..5293bd70d5
--- /dev/null
+++ b/frontend/src/components/ActionButton.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/AutoRefreshDataView.vue b/frontend/src/components/AutoRefreshDataView.vue
new file mode 100644
index 0000000000..2fb2757a1d
--- /dev/null
+++ b/frontend/src/components/AutoRefreshDataView.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/BackendChecksNotifications.vue b/frontend/src/components/BackendChecksNotifications.vue
new file mode 100644
index 0000000000..0804ce809b
--- /dev/null
+++ b/frontend/src/components/BackendChecksNotifications.vue
@@ -0,0 +1,51 @@
+
+
+
+
diff --git a/frontend/src/components/CodeEditor.vue b/frontend/src/components/CodeEditor.vue
new file mode 100644
index 0000000000..c74dc9a077
--- /dev/null
+++ b/frontend/src/components/CodeEditor.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ColumnHeader.vue b/frontend/src/components/ColumnHeader.vue
new file mode 100644
index 0000000000..0e2c5acb01
--- /dev/null
+++ b/frontend/src/components/ColumnHeader.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ConditionalRender.vue b/frontend/src/components/ConditionalRender.vue
new file mode 100644
index 0000000000..350521cada
--- /dev/null
+++ b/frontend/src/components/ConditionalRender.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ConfirmDialog.vue b/frontend/src/components/ConfirmDialog.vue
new file mode 100644
index 0000000000..f332e4e5af
--- /dev/null
+++ b/frontend/src/components/ConfirmDialog.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
{{ body }}
+
{{ secondParagraph }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/CopyToClipboard.vue b/frontend/src/components/CopyToClipboard.vue
new file mode 100644
index 0000000000..e30c88ab1f
--- /dev/null
+++ b/frontend/src/components/CopyToClipboard.vue
@@ -0,0 +1,33 @@
+
+
+
+
+ Copy to clipboard
+
+
+
diff --git a/frontend/src/components/DashboardItem.vue b/frontend/src/components/DashboardItem.vue
new file mode 100644
index 0000000000..4cbdc7e5de
--- /dev/null
+++ b/frontend/src/components/DashboardItem.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+ {{ counter }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/DataView.vue b/frontend/src/components/DataView.vue
new file mode 100644
index 0000000000..6bc625c5d5
--- /dev/null
+++ b/frontend/src/components/DataView.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/frontend/src/components/DataViewPageModel.ts b/frontend/src/components/DataViewPageModel.ts
new file mode 100644
index 0000000000..84474be8a1
--- /dev/null
+++ b/frontend/src/components/DataViewPageModel.ts
@@ -0,0 +1,4 @@
+export default interface DataViewPageModel {
+ data: T[];
+ totalCount: number;
+}
diff --git a/frontend/src/components/DropDown.vue b/frontend/src/components/DropDown.vue
new file mode 100644
index 0000000000..6d5fba6577
--- /dev/null
+++ b/frontend/src/components/DropDown.vue
@@ -0,0 +1,46 @@
+
+
+
+
+ {{ label }}:
+
+
+
+
+
+
diff --git a/frontend/src/components/EventItemShort.vue b/frontend/src/components/EventItemShort.vue
new file mode 100644
index 0000000000..29fa3e2eb9
--- /dev/null
+++ b/frontend/src/components/EventItemShort.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
Last 10 events
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/EventLogItem.vue b/frontend/src/components/EventLogItem.vue
new file mode 100644
index 0000000000..9ed77f02c9
--- /dev/null
+++ b/frontend/src/components/EventLogItem.vue
@@ -0,0 +1,176 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ExclamationMark.vue b/frontend/src/components/ExclamationMark.vue
new file mode 100644
index 0000000000..504e51b314
--- /dev/null
+++ b/frontend/src/components/ExclamationMark.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/FAIcon.vue b/frontend/src/components/FAIcon.vue
new file mode 100644
index 0000000000..8d550f9cb0
--- /dev/null
+++ b/frontend/src/components/FAIcon.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/frontend/src/components/FeedbackButton.vue b/frontend/src/components/FeedbackButton.vue
new file mode 100644
index 0000000000..216a8786e6
--- /dev/null
+++ b/frontend/src/components/FeedbackButton.vue
@@ -0,0 +1,38 @@
+
+
+
+
+
+ Feedback
+
+
+
diff --git a/frontend/src/components/FilterInput.vue b/frontend/src/components/FilterInput.vue
new file mode 100644
index 0000000000..b4f92350be
--- /dev/null
+++ b/frontend/src/components/FilterInput.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+ emit('focus')" @blur="() => emit('blur')" :placeholder="props.placeholder" :aria-label="props.ariaLabel" class="form-control filter-input" v-model="localInput" />
+
+
+
+
diff --git a/frontend/src/components/ItemsPerPage.vue b/frontend/src/components/ItemsPerPage.vue
new file mode 100644
index 0000000000..6bce88e5a0
--- /dev/null
+++ b/frontend/src/components/ItemsPerPage.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/LicenseExpired.vue b/frontend/src/components/LicenseExpired.vue
new file mode 100644
index 0000000000..8e31f92e0d
--- /dev/null
+++ b/frontend/src/components/LicenseExpired.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
Platform license expired
+
Please update your license to continue using the Particular Service Platform
+
+ View license details
+
+
+
+
+
+
License expired
+
To continue using the Particular Service Platform, please extend your license
+
+
+
+
+
+
Platform license expired
+
Your upgrade protection period has elapsed and your license is not valid for this version of ServicePulse.
+
+ View license details
+
+
+
+
diff --git a/frontend/src/components/LicenseNotExpired.vue b/frontend/src/components/LicenseNotExpired.vue
new file mode 100644
index 0000000000..e6e23d0036
--- /dev/null
+++ b/frontend/src/components/LicenseNotExpired.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/LicenseNotifications.vue b/frontend/src/components/LicenseNotifications.vue
new file mode 100644
index 0000000000..dc9c6441aa
--- /dev/null
+++ b/frontend/src/components/LicenseNotifications.vue
@@ -0,0 +1,68 @@
+
+
+
+
diff --git a/frontend/src/components/LoadingSpinner.vue b/frontend/src/components/LoadingSpinner.vue
new file mode 100644
index 0000000000..38d1b9ebb1
--- /dev/null
+++ b/frontend/src/components/LoadingSpinner.vue
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/frontend/src/components/MaximizableCodeEditor.vue b/frontend/src/components/MaximizableCodeEditor.vue
new file mode 100644
index 0000000000..57ffa635be
--- /dev/null
+++ b/frontend/src/components/MaximizableCodeEditor.vue
@@ -0,0 +1,188 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/MetadataItem.vue b/frontend/src/components/MetadataItem.vue
new file mode 100644
index 0000000000..ad84f02d91
--- /dev/null
+++ b/frontend/src/components/MetadataItem.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/NoData.spec.ts b/frontend/src/components/NoData.spec.ts
new file mode 100644
index 0000000000..e45241062c
--- /dev/null
+++ b/frontend/src/components/NoData.spec.ts
@@ -0,0 +1,8 @@
+import { expect, test, render, screen } from "@component-test-utils";
+
+import NoData from "./NoData.vue";
+
+test("EXAMPLE: A messge non empty message is assigned", async () => {
+ render(NoData, { props: { message: "No messages processed in this period of time" } });
+ expect(await screen.findByText("No messages processed in this period of time")).toBeVisible();
+});
diff --git a/frontend/src/components/NoData.vue b/frontend/src/components/NoData.vue
new file mode 100644
index 0000000000..8725df681e
--- /dev/null
+++ b/frontend/src/components/NoData.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/OnOffSwitch.vue b/frontend/src/components/OnOffSwitch.vue
new file mode 100644
index 0000000000..5e94948ef6
--- /dev/null
+++ b/frontend/src/components/OnOffSwitch.vue
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/OrderBy.vue b/frontend/src/components/OrderBy.vue
new file mode 100644
index 0000000000..81288dcc9c
--- /dev/null
+++ b/frontend/src/components/OrderBy.vue
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/PageFooter.vue b/frontend/src/components/PageFooter.vue
new file mode 100644
index 0000000000..387610b94d
--- /dev/null
+++ b/frontend/src/components/PageFooter.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/PageHeader.vue b/frontend/src/components/PageHeader.vue
new file mode 100644
index 0000000000..640f2dcdd4
--- /dev/null
+++ b/frontend/src/components/PageHeader.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/PaginationStrip.spec.ts b/frontend/src/components/PaginationStrip.spec.ts
new file mode 100644
index 0000000000..834859f95b
--- /dev/null
+++ b/frontend/src/components/PaginationStrip.spec.ts
@@ -0,0 +1,275 @@
+import { expect, render, screen, describe, userEvent, test } from "@component-test-utils";
+import paginationStrip from "./PaginationStrip.vue";
+
+//Defines a domain-specific language (DSL) for interacting with the system under test (sut)
+interface PaginationStripDSL {
+ clickPrevious(): Promise;
+ clickNext(): Promise;
+ clickPage(pageName: string): Promise;
+ clickJumpPagesForward(): Promise;
+ clickJumpPagesBack(): Promise;
+ updateNumberOfRecordsPerPage(newNumberOfItemsPerPage: number): Promise;
+ assert: PaginationStripDSLAssertions;
+}
+
+//Defines a domain-specific language (DSL) for checking assertions against the system under test (sut)
+interface PaginationStripDSLAssertions {
+ stripOfButtonsMatchesSequence(value: string): void;
+ activePageIs(value: string): void;
+ previousIsEnabled(): void;
+ previousIsDisabled(): void;
+ nextIsEnabled(): void;
+ nextIsDisabled(): void;
+ jumpPagesBackIsPresent(value?: boolean): void;
+ jumpPagesForwardIsPresent(value?: boolean): void;
+}
+
+describe("Feature: Moving backwards through pages with a single button must be possible", () => {
+ describe("Rule: The 'Previous' button is disabled when the first page is active", () => {
+ test("EXAMPLE: First page is active on the initial render", () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 1 });
+
+ component.assert.previousIsDisabled();
+ });
+
+ test("EXAMPLE: Clicking 'previous' button from second page", async () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 2 });
+
+ component.assert.previousIsEnabled();
+
+ await component.clickPrevious();
+
+ component.assert.previousIsDisabled();
+ component.assert.activePageIs("Page 1");
+ });
+ });
+ describe("Rule: The 'Previous' button is enabled when the first page is not active", () => {
+ test("EXAMPLE: Second page is active on initial render", () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 2 });
+ component.assert.previousIsEnabled();
+ });
+
+ test("EXAMPLE: Clicking 'Next' button from first page", async () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 1 });
+
+ component.assert.previousIsDisabled();
+
+ await component.clickNext();
+
+ component.assert.activePageIs("Page 2");
+ component.assert.previousIsEnabled();
+ });
+ });
+});
+
+describe("Feature: Moving forward through pages with a single button must be possible", () => {
+ describe("Rule: The 'Next' button is disabled when the last page is active", () => {
+ test("EXAMPLE: Last page is active on the initial render", () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 10 });
+ component.assert.nextIsDisabled();
+ });
+
+ test("EXAMPLE: Clicking 'Next' button from penultimate page", async () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 9 });
+
+ component.assert.nextIsEnabled();
+
+ await component.clickNext();
+
+ component.assert.nextIsDisabled();
+ component.assert.activePageIs("Page 10");
+ });
+ });
+ describe("Rule: The 'Next' button is enabled when the last page is not active", () => {
+ test("EXAMPLE: Penultimate page is active on initial render", () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 9 });
+
+ component.assert.nextIsEnabled();
+ });
+
+ test("EXAMPLE: Clicking 'Previous' button from last page", async () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 10 });
+
+ component.assert.nextIsDisabled();
+
+ await component.clickPrevious();
+ component.assert.nextIsEnabled();
+ component.assert.activePageIs("Page 9");
+ });
+ });
+});
+
+describe("Feature: Navigating to a specific page that is available must be possible", () => {
+ describe("Rule: Clicking to an specific page should show the page as active", () => {
+ test("EXAMPLE: First page is active then clicking to page number 4", async () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 1, allowToJumpPagesBy: 2 });
+
+ component.assert.stripOfButtonsMatchesSequence("Previous,1,2,3,4,...,10,Next");
+
+ await component.clickPage("Page 4");
+ component.assert.activePageIs("Page 4");
+
+ component.assert.stripOfButtonsMatchesSequence("Previous,1,...,2,3,4,5,6,...,10,Next");
+ });
+ });
+});
+
+describe("Feature: Jumping a number of pages forward or backward must be possible", () => {
+ describe("Rule: Buttons for jumping pages back or forward are available only when enough pages ahead or back are available", () => {
+ test("EXAMPLE: Strip for 100 records with 10 items per page, allowing to jump pages by 2", () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 1, allowToJumpPagesBy: 2 });
+
+ component.assert.stripOfButtonsMatchesSequence("Previous,1,2,3,4,...,10,Next");
+ });
+
+ test("EXAMPLE: Enough pages to jump forward and backward", () => {
+ const component = renderPaginationStripWith({ records: 500, itemsPerPage: 10, selectedPage: 10, allowToJumpPagesBy: 5 });
+
+ component.assert.jumpPagesBackIsPresent();
+ component.assert.jumpPagesForwardIsPresent();
+ });
+
+ test("EXAMPLE: Enough pages to jump foward only", () => {
+ const component = renderPaginationStripWith({ records: 500, itemsPerPage: 10, selectedPage: 6, allowToJumpPagesBy: 5 });
+
+ component.assert.jumpPagesBackIsPresent(false);
+ component.assert.jumpPagesForwardIsPresent();
+ });
+
+ test("EXAMPLE: Enough pages to jump back only", () => {
+ const component = renderPaginationStripWith({ records: 500, itemsPerPage: 10, selectedPage: 50, allowToJumpPagesBy: 5 });
+
+ component.assert.jumpPagesBackIsPresent();
+ component.assert.jumpPagesForwardIsPresent(false);
+ component.assert.activePageIs("Page 50");
+ });
+
+ test("EXAMPLE: Not enough pages to jump forward or backward", () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 1, allowToJumpPagesBy: 5 });
+
+ component.assert.jumpPagesBackIsPresent(false);
+ component.assert.jumpPagesForwardIsPresent(false);
+ });
+
+ test("EXAMPLE: Jump 5 pages forward", async () => {
+ const component = renderPaginationStripWith({ records: 500, itemsPerPage: 10, selectedPage: 6, allowToJumpPagesBy: 5 });
+
+ component.assert.jumpPagesBackIsPresent(false);
+ component.assert.jumpPagesForwardIsPresent();
+
+ await component.clickJumpPagesForward();
+
+ component.assert.jumpPagesBackIsPresent();
+ component.assert.jumpPagesForwardIsPresent();
+
+ component.assert.activePageIs("Page 11");
+ });
+
+ test("EXAMPLE: Jump 10 pages back", async () => {
+ const component = renderPaginationStripWith({ records: 500, itemsPerPage: 10, selectedPage: 50, allowToJumpPagesBy: 5 });
+
+ component.assert.jumpPagesBackIsPresent();
+ component.assert.jumpPagesForwardIsPresent(false);
+
+ await component.clickJumpPagesBack();
+ await component.clickJumpPagesBack();
+
+ component.assert.jumpPagesBackIsPresent();
+ component.assert.jumpPagesForwardIsPresent();
+
+ component.assert.activePageIs("Page 40");
+ });
+ });
+});
+
+describe("Feature: changes in the number of records per page are allowed", () => {
+ describe("Rule: Updating the number of records per page recalculates the pages of the strip and resets selected page", () => {
+ test("EXAMPLE: Number of records per page gets updated from 10 to 50", async () => {
+ const component = renderPaginationStripWith({ records: 100, itemsPerPage: 10, selectedPage: 3, allowToJumpPagesBy: 2 });
+
+ component.assert.stripOfButtonsMatchesSequence("Previous,1,2,3,4,5,...,10,Next");
+ component.assert.activePageIs("Page 3");
+
+ await component.updateNumberOfRecordsPerPage(50);
+ component.assert.stripOfButtonsMatchesSequence("Previous,1,2,Next");
+
+ component.assert.activePageIs("Page 1");
+ });
+ });
+});
+
+function renderPaginationStripWith({ records, itemsPerPage, selectedPage, allowToJumpPagesBy = 0 }: { records: number; itemsPerPage: number; selectedPage: number; allowToJumpPagesBy?: number }): PaginationStripDSL {
+ const { rerender } = render(paginationStrip, {
+ props: {
+ modelValue: selectedPage,
+ itemsPerPage: itemsPerPage,
+ totalCount: records,
+ pageBuffer: allowToJumpPagesBy,
+ },
+ });
+
+ const dslAPI: PaginationStripDSL = {
+ clickPrevious: async function () {
+ await userEvent.click(await screen.findByLabelText("Previous Page"));
+ },
+
+ clickNext: async function () {
+ await userEvent.click(await screen.findByLabelText("Next Page"));
+ },
+
+ clickJumpPagesForward: async function () {
+ await userEvent.click(await screen.findByLabelText(`Forward ${allowToJumpPagesBy}`));
+ },
+
+ clickJumpPagesBack: async function () {
+ await userEvent.click(await screen.findByLabelText(`Back ${allowToJumpPagesBy}`));
+ },
+ clickPage: async function (pageName: string): Promise {
+ await userEvent.click(await screen.findByLabelText(pageName));
+ },
+ updateNumberOfRecordsPerPage: async function (newNumberOfItemsPerPage: number) {
+ await rerender({
+ modelValue: selectedPage,
+ itemsPerPage: newNumberOfItemsPerPage,
+ totalCount: records,
+ pageBuffer: allowToJumpPagesBy,
+ });
+ },
+ assert: {
+ previousIsDisabled: function () {
+ expect(screen.queryByLabelText("Previous Page")).toBeDisabled();
+ },
+ previousIsEnabled: function () {
+ expect(screen.queryByLabelText("Previous Page")).toBeEnabled();
+ },
+ nextIsDisabled: function () {
+ expect(screen.queryByLabelText("Next Page")).toBeDisabled();
+ },
+ nextIsEnabled: function () {
+ expect(screen.queryByLabelText("Next Page")).toBeEnabled();
+ },
+ activePageIs: function (value) {
+ expect(screen.getByRole("button", { pressed: true, name: value })).toBeInTheDocument();
+ },
+ jumpPagesBackIsPresent: function (truthy = true) {
+ if (truthy) {
+ expect(screen.queryByLabelText(`Back ${allowToJumpPagesBy}`)).toBeInTheDocument();
+ }
+ },
+ jumpPagesForwardIsPresent: function (truthy = true) {
+ if (truthy) {
+ expect(screen.queryByLabelText(`Forward ${allowToJumpPagesBy}`)).toBeInTheDocument();
+ } else {
+ expect(screen.queryByLabelText(`Forward ${allowToJumpPagesBy}`)).not.toBeInTheDocument();
+ }
+ },
+ stripOfButtonsMatchesSequence: function (sequence: string): void {
+ const allButtons = screen.getAllByRole("button");
+ const generatedStripText = allButtons.map((v) => v.innerHTML).join(",");
+ expect(generatedStripText).toBe(sequence);
+ },
+ },
+ };
+
+ return dslAPI;
+}
diff --git a/frontend/src/components/PaginationStrip.vue b/frontend/src/components/PaginationStrip.vue
new file mode 100644
index 0000000000..3f11a59f0f
--- /dev/null
+++ b/frontend/src/components/PaginationStrip.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/RefreshConfig.vue b/frontend/src/components/RefreshConfig.vue
new file mode 100644
index 0000000000..e03ffab0c1
--- /dev/null
+++ b/frontend/src/components/RefreshConfig.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ResultsCount.vue b/frontend/src/components/ResultsCount.vue
new file mode 100644
index 0000000000..2340aa8c7b
--- /dev/null
+++ b/frontend/src/components/ResultsCount.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/SagaName.vue b/frontend/src/components/SagaName.vue
new file mode 100644
index 0000000000..b1ea343aa1
--- /dev/null
+++ b/frontend/src/components/SagaName.vue
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/ServiceControlAvailable.vue b/frontend/src/components/ServiceControlAvailable.vue
new file mode 100644
index 0000000000..17493cf28a
--- /dev/null
+++ b/frontend/src/components/ServiceControlAvailable.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
Cannot connect to ServiceControl
+
+ ServicePulse is unable to connect to the ServiceControl instance at
+ {{ serviceControlUrl }} . Please ensure that ServiceControl is running and accessible from your machine.
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/SortInfo.ts b/frontend/src/components/SortInfo.ts
new file mode 100644
index 0000000000..b0f123b936
--- /dev/null
+++ b/frontend/src/components/SortInfo.ts
@@ -0,0 +1,4 @@
+export interface SortInfo {
+ property: string;
+ isAscending: boolean;
+}
diff --git a/frontend/src/components/StatusIcon.vue b/frontend/src/components/StatusIcon.vue
new file mode 100644
index 0000000000..0a0e5de6c1
--- /dev/null
+++ b/frontend/src/components/StatusIcon.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+ {{ displayMessage }}
+
+
+
+
diff --git a/frontend/src/components/TabsLayout.vue b/frontend/src/components/TabsLayout.vue
new file mode 100644
index 0000000000..184ab6fd61
--- /dev/null
+++ b/frontend/src/components/TabsLayout.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/TextEllipses.vue b/frontend/src/components/TextEllipses.vue
new file mode 100644
index 0000000000..bb589e449f
--- /dev/null
+++ b/frontend/src/components/TextEllipses.vue
@@ -0,0 +1,53 @@
+
+
+
+
+ {{ text }}
+
+
+
+
diff --git a/frontend/src/components/TimeSince.vue b/frontend/src/components/TimeSince.vue
new file mode 100644
index 0000000000..0a75a58693
--- /dev/null
+++ b/frontend/src/components/TimeSince.vue
@@ -0,0 +1,47 @@
+
+
+
+ {{ text }}
+
diff --git a/frontend/src/components/ToastPopup.vue b/frontend/src/components/ToastPopup.vue
new file mode 100644
index 0000000000..39701c070f
--- /dev/null
+++ b/frontend/src/components/ToastPopup.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
+
+ {{ props.title }}
+
+
+
+
+
+
diff --git a/frontend/src/components/TypeNameDisplay.vue b/frontend/src/components/TypeNameDisplay.vue
new file mode 100644
index 0000000000..917240a46b
--- /dev/null
+++ b/frontend/src/components/TypeNameDisplay.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/WarningLevel.ts b/frontend/src/components/WarningLevel.ts
new file mode 100644
index 0000000000..eef486b287
--- /dev/null
+++ b/frontend/src/components/WarningLevel.ts
@@ -0,0 +1,5 @@
+export enum WarningLevel {
+ None = "none",
+ Warning = "warning",
+ Danger = "danger",
+}
diff --git a/frontend/src/components/audit/AuditList.vue b/frontend/src/components/audit/AuditList.vue
new file mode 100644
index 0000000000..1004e71133
--- /dev/null
+++ b/frontend/src/components/audit/AuditList.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/audit/AuditListItem.vue b/frontend/src/components/audit/AuditListItem.vue
new file mode 100644
index 0000000000..cc107068b6
--- /dev/null
+++ b/frontend/src/components/audit/AuditListItem.vue
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+ {{ props.message.message_id }}
+ {{ props.message.message_type }}
+ Time Sent: {{ new Date(props.message.time_sent).toLocaleString() }}
+ Critical Time: {{ formatDotNetTimespan(props.message.critical_time) }}
+ Processing Time: {{ formatDotNetTimespan(props.message.processing_time) }}
+ Delivery Time: {{ formatDotNetTimespan(props.message.delivery_time) }}
+
+
+
+
diff --git a/frontend/src/components/audit/AuditMenuItem.vue b/frontend/src/components/audit/AuditMenuItem.vue
new file mode 100644
index 0000000000..ff80a9061d
--- /dev/null
+++ b/frontend/src/components/audit/AuditMenuItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+ All Messages
+
+
+
+
diff --git a/frontend/src/components/audit/DatePickerRange.vue b/frontend/src/components/audit/DatePickerRange.vue
new file mode 100644
index 0000000000..be40ddf95f
--- /dev/null
+++ b/frontend/src/components/audit/DatePickerRange.vue
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+ Clear Range
+
+
+
+
+
diff --git a/frontend/src/components/audit/FiltersPanel.vue b/frontend/src/components/audit/FiltersPanel.vue
new file mode 100644
index 0000000000..cc21d3cb54
--- /dev/null
+++ b/frontend/src/components/audit/FiltersPanel.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/audit/ListFilterSelector.vue b/frontend/src/components/audit/ListFilterSelector.vue
new file mode 100644
index 0000000000..dbe8df59ce
--- /dev/null
+++ b/frontend/src/components/audit/ListFilterSelector.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/audit/MessageStatusIcon.vue b/frontend/src/components/audit/MessageStatusIcon.vue
new file mode 100644
index 0000000000..40092cfa78
--- /dev/null
+++ b/frontend/src/components/audit/MessageStatusIcon.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/audit/isAllMessagesSupported.ts b/frontend/src/components/audit/isAllMessagesSupported.ts
new file mode 100644
index 0000000000..e3d76561fc
--- /dev/null
+++ b/frontend/src/components/audit/isAllMessagesSupported.ts
@@ -0,0 +1,8 @@
+import useEnvironmentAndVersionsAutoRefresh from "@/composables/useEnvironmentAndVersionsAutoRefresh";
+
+export const minimumSCVersionForAllMessages = "6.6.0";
+
+export default function useIsAllMessagesSupported() {
+ const { store: environmentStore } = useEnvironmentAndVersionsAutoRefresh();
+ return environmentStore.serviceControlIsGreaterThan(minimumSCVersionForAllMessages);
+}
diff --git a/frontend/src/components/codeEditorTypes.ts b/frontend/src/components/codeEditorTypes.ts
new file mode 100644
index 0000000000..c1f7bcd46f
--- /dev/null
+++ b/frontend/src/components/codeEditorTypes.ts
@@ -0,0 +1 @@
+export type CodeLanguage = "json" | "xml" | "shell" | "powershell" | "csharp";
diff --git a/frontend/src/components/configuration/ConfigurationMenuItem.vue b/frontend/src/components/configuration/ConfigurationMenuItem.vue
new file mode 100644
index 0000000000..fedab35c36
--- /dev/null
+++ b/frontend/src/components/configuration/ConfigurationMenuItem.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Configuration
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/EmailSettings.ts b/frontend/src/components/configuration/EmailSettings.ts
new file mode 100644
index 0000000000..725b07323a
--- /dev/null
+++ b/frontend/src/components/configuration/EmailSettings.ts
@@ -0,0 +1,10 @@
+export default interface EmailSettings {
+ enabled: boolean | null;
+ enable_tls: boolean | null;
+ smtp_server: string;
+ smtp_port: number | null;
+ authentication_account: string;
+ authentication_password: string;
+ from: string;
+ to: string;
+}
diff --git a/frontend/src/components/configuration/EndpointConnection.vue b/frontend/src/components/configuration/EndpointConnection.vue
new file mode 100644
index 0000000000..86669e16a3
--- /dev/null
+++ b/frontend/src/components/configuration/EndpointConnection.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+
+
Connect an endpoint to ServiceControl
+
+
+
+
+
+
+
+
+
+
+
+ There were problems reaching some ServiceControl instances and the configuration does not contain all connectivity information.
+
+
+
+
+
+
+
+
+
Note that when using JSON for configuration, you also need to change the endpoint configuration as shown below.
+
Endpoint configuration:
+
+
+ JSON configuration file:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/HealthCheckNotifications.vue b/frontend/src/components/configuration/HealthCheckNotifications.vue
new file mode 100644
index 0000000000..cc23cd2195
--- /dev/null
+++ b/frontend/src/components/configuration/HealthCheckNotifications.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
Configure notifications for health checks built into ServiceControl (low disk space, stale database indexes, audit ingestion, etc.).
+
+
+
+
+
+
+
+
+
+
+
+ Update failed
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/HealthCheckNotifications_ConfigureEmail.vue b/frontend/src/components/configuration/HealthCheckNotifications_ConfigureEmail.vue
new file mode 100644
index 0000000000..f650b420d6
--- /dev/null
+++ b/frontend/src/components/configuration/HealthCheckNotifications_ConfigureEmail.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/MassTransitConnector.vue b/frontend/src/components/configuration/MassTransitConnector.vue
new file mode 100644
index 0000000000..81d0c7ed78
--- /dev/null
+++ b/frontend/src/components/configuration/MassTransitConnector.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+ Connector Version: {{ configuration.mass_transit_connector.version }}
+
+
+
+
List of error queues configured in the connector.
+
+
+
+ {{ queue.name }}
+
+
+
+
+
The entries below are the most recent warning and error-level events recorded on the ServiceControl Connector.
+
+
No warning or error logs
+
+
{{ formatDate(log.date) }}
+
{{ log.level }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/PlatformConnections.vue b/frontend/src/components/configuration/PlatformConnections.vue
new file mode 100644
index 0000000000..a853ed52ed
--- /dev/null
+++ b/frontend/src/components/configuration/PlatformConnections.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/PlatformLicense.vue b/frontend/src/components/configuration/PlatformLicense.vue
new file mode 100644
index 0000000000..bdfbf09424
--- /dev/null
+++ b/frontend/src/components/configuration/PlatformLicense.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Platform license type: {{ typeText(license, configuration) }}{{ licenseEdition }}
+
+
+
+
+
License expiry date:
+
+ {{ formattedExpirationDate }}
+ {{ licenseStatus.subscriptionDaysLeft }}
+
+
+
Your license expired. Please update the license to continue using the Particular Service Platform.
+
+
+
+
+
License expiry date:
+
+ {{ formattedExpirationDate }}
+ {{ licenseStatus.trialDaysLeft }}
+
+
+
+ Your license expired. To continue using the Particular Service Platform you'll need to extend your license.
+
+
+
+
+
+
+
+ Upgrade protection expiry date:
+
+ {{ formattedUpgradeProtectionExpiration }}
+ {{ licenseStatus.upgradeDaysLeft }}
+
+
+
+
+ Warning: Once upgrade protection expires, you'll no longer have access to support or new product versions.
+
+
Your license upgrade protection expired before this version of ServicePulse was released.
+
+
+
+ ServiceControl instance:
+ {{ formattedInstanceName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/RetryRedirectEdit.vue b/frontend/src/components/configuration/RetryRedirectEdit.vue
new file mode 100644
index 0000000000..56da3e8d72
--- /dev/null
+++ b/frontend/src/components/configuration/RetryRedirectEdit.vue
@@ -0,0 +1,174 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/RetryRedirects.vue b/frontend/src/components/configuration/RetryRedirects.vue
new file mode 100644
index 0000000000..f9f5d4a4b1
--- /dev/null
+++ b/frontend/src/components/configuration/RetryRedirects.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ redirect.from_physical_address }}
+
+
+
+ {{ redirect.to_physical_address }}
+
+
+
+ Last modified:
+
+
+
+
+
+
+ End Redirect
+ Modify Redirect
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/configuration/convertToWarningLevel.ts b/frontend/src/components/configuration/convertToWarningLevel.ts
new file mode 100644
index 0000000000..b808e88193
--- /dev/null
+++ b/frontend/src/components/configuration/convertToWarningLevel.ts
@@ -0,0 +1,13 @@
+import { LicenseWarningLevel } from "@/composables/LicenseStatus";
+import { WarningLevel } from "@/components/WarningLevel";
+
+export default function convertToWarningLevel(level: LicenseWarningLevel) {
+ switch (level) {
+ case LicenseWarningLevel.None:
+ return WarningLevel.None;
+ case LicenseWarningLevel.Warning:
+ return WarningLevel.Warning;
+ case LicenseWarningLevel.Danger:
+ return WarningLevel.Danger;
+ }
+}
diff --git a/frontend/src/components/customchecks/CustomCheckView.vue b/frontend/src/components/customchecks/CustomCheckView.vue
new file mode 100644
index 0000000000..ccb49ada81
--- /dev/null
+++ b/frontend/src/components/customchecks/CustomCheckView.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/customchecks/CustomChecksDashboardItem.vue b/frontend/src/components/customchecks/CustomChecksDashboardItem.vue
new file mode 100644
index 0000000000..113060771d
--- /dev/null
+++ b/frontend/src/components/customchecks/CustomChecksDashboardItem.vue
@@ -0,0 +1,14 @@
+
+
+
+ Custom Checks
+
diff --git a/frontend/src/components/customchecks/CustomChecksMenuItem.vue b/frontend/src/components/customchecks/CustomChecksMenuItem.vue
new file mode 100644
index 0000000000..b3c4542f68
--- /dev/null
+++ b/frontend/src/components/customchecks/CustomChecksMenuItem.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Custom Checks
+ {{ failingCount }}
+
+
+
+
diff --git a/frontend/src/components/dashboard/DashboardMenuItem.vue b/frontend/src/components/dashboard/DashboardMenuItem.vue
new file mode 100644
index 0000000000..28d1efdfa9
--- /dev/null
+++ b/frontend/src/components/dashboard/DashboardMenuItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Dashboard
+
+
+
+
diff --git a/frontend/src/components/events/EventsMenuItem.vue b/frontend/src/components/events/EventsMenuItem.vue
new file mode 100644
index 0000000000..a1dd1a6da9
--- /dev/null
+++ b/frontend/src/components/events/EventsMenuItem.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Events
+
+
+
+
diff --git a/frontend/src/components/failedmessages/DeletedMessageGroups.vue b/frontend/src/components/failedmessages/DeletedMessageGroups.vue
new file mode 100644
index 0000000000..b3f0a1242f
--- /dev/null
+++ b/frontend/src/components/failedmessages/DeletedMessageGroups.vue
@@ -0,0 +1,439 @@
+
+
+
+
+
+
+
+
+
Deleted message group
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Restore request started:
+ Messages restored: {{ group.count }}
+ Messages being restored: {{ group.count }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/DeletedMessages.vue b/frontend/src/components/failedmessages/DeletedMessages.vue
new file mode 100644
index 0000000000..0bd50627bc
--- /dev/null
+++ b/frontend/src/components/failedmessages/DeletedMessages.vue
@@ -0,0 +1,287 @@
+
+
+
+
+
+
+
+
+
+ {{ groupName }}
+
+ {{ totalCount }} messages in group
+
+
+
+
+
+ Select all
+ Clear selection
+ Restore {{ numberSelected() }} selected
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/EditIgnoredDialog.vue b/frontend/src/components/failedmessages/EditIgnoredDialog.vue
new file mode 100644
index 0000000000..8d54c56463
--- /dev/null
+++ b/frontend/src/components/failedmessages/EditIgnoredDialog.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
This retry was ignored because another edit had already been processed.
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/EditMessageHeader.vue b/frontend/src/components/failedmessages/EditMessageHeader.vue
new file mode 100644
index 0000000000..e0013b67c2
--- /dev/null
+++ b/frontend/src/components/failedmessages/EditMessageHeader.vue
@@ -0,0 +1,134 @@
+
+
+
+
+ {{ settings.header.key }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/EditRetryDialog.vue b/frontend/src/components/failedmessages/EditRetryDialog.vue
new file mode 100644
index 0000000000..5393f459aa
--- /dev/null
+++ b/frontend/src/components/failedmessages/EditRetryDialog.vue
@@ -0,0 +1,328 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This message is an event. If it was already successfully handled by other subscribers, editing it now has the risk of changing the semantic meaning of the event and may result in
+ altering the system behavior.
+
+
+
+
+ Message body cannot be edited because content type "{{ localMessage.bodyContentType }}" is not supported. Only messages with content types "application/json" and "text/xml" can be
+ edited.
+
+
+
+
An error occurred while retrying the message, please check the ServiceControl logs for more details on the failure.
+
+
+
+
+
+
+
+
+ Message body cannot be empty
+
+
+ Reset changes
+
+
+
+
+ {{ localMessage.bodyUnavailable }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/FailedMessageGroupNoteEdit.vue b/frontend/src/components/failedmessages/FailedMessageGroupNoteEdit.vue
new file mode 100644
index 0000000000..1804cd4e87
--- /dev/null
+++ b/frontend/src/components/failedmessages/FailedMessageGroupNoteEdit.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/FailedMessageGroups.vue b/frontend/src/components/failedmessages/FailedMessageGroups.vue
new file mode 100644
index 0000000000..00c56cf9a1
--- /dev/null
+++ b/frontend/src/components/failedmessages/FailedMessageGroups.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
Failed message group
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/FailedMessages.vue b/frontend/src/components/failedmessages/FailedMessages.vue
new file mode 100644
index 0000000000..04c88cc2b1
--- /dev/null
+++ b/frontend/src/components/failedmessages/FailedMessages.vue
@@ -0,0 +1,356 @@
+
+
+
+
+
+
+
+
+
+ {{ groupName }}
+
+ {{ totalCount }} messages in group
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/FailedMessagesDashboardItem.vue b/frontend/src/components/failedmessages/FailedMessagesDashboardItem.vue
new file mode 100644
index 0000000000..5d2f4931ba
--- /dev/null
+++ b/frontend/src/components/failedmessages/FailedMessagesDashboardItem.vue
@@ -0,0 +1,14 @@
+
+
+
+ Failed Messages
+
diff --git a/frontend/src/components/failedmessages/FailedMessagesMenuItem.vue b/frontend/src/components/failedmessages/FailedMessagesMenuItem.vue
new file mode 100644
index 0000000000..92cd5da021
--- /dev/null
+++ b/frontend/src/components/failedmessages/FailedMessagesMenuItem.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Failed Messages
+ {{ failedMessageCount }}
+
+
+
+
diff --git a/frontend/src/components/failedmessages/LastTenOperations.vue b/frontend/src/components/failedmessages/LastTenOperations.vue
new file mode 100644
index 0000000000..b146bf38e7
--- /dev/null
+++ b/frontend/src/components/failedmessages/LastTenOperations.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+ There is only {{ historicOperations.length }} completed group retry
+ There are only {{ historicOperations.length }} completed group retries
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/MessageGroupList.vue b/frontend/src/components/failedmessages/MessageGroupList.vue
new file mode 100644
index 0000000000..a6efa23ca1
--- /dev/null
+++ b/frontend/src/components/failedmessages/MessageGroupList.vue
@@ -0,0 +1,647 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ NOTE: {{ group.comment }}
+
+
+
+
+
+
+ Request retry
+
+
+
+ Delete group
+
+
Add note
+
Edit note
+
Remove note
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.workflow_state.status === "completed" ? "Messages sent:" : "Messages to send:" }} {{ group.operation_remaining_count || group.count }}
+ Retry request started:
+ Retry request completed:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete request started:
+
+
+
+ Messages left to delete:
+ {{ group.operation_remaining_count || 0 }}
+
+
+
+ Messages deleted:
+ {{ group.operation_messages_completed_count || 0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/MessageList.vue b/frontend/src/components/failedmessages/MessageList.vue
new file mode 100644
index 0000000000..7fd88e9107
--- /dev/null
+++ b/frontend/src/components/failedmessages/MessageList.vue
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/PendingRetries.vue b/frontend/src/components/failedmessages/PendingRetries.vue
new file mode 100644
index 0000000000..ff5050c294
--- /dev/null
+++ b/frontend/src/components/failedmessages/PendingRetries.vue
@@ -0,0 +1,421 @@
+
+
+
+
+
+
+
+
+
+
To check if a retried message was also processed successfully, enable
+
message auditing
+
+
+
+
MassTransit endpoints currently do not report when a pending retry has succeeded, and therefore any messages associated with those endpoints will need to be manually marked as resolved.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/ProgressMessage.vue b/frontend/src/components/failedmessages/ProgressMessage.vue
new file mode 100644
index 0000000000..bb27a5a191
--- /dev/null
+++ b/frontend/src/components/failedmessages/ProgressMessage.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/failedmessages/failedmessages.css b/frontend/src/components/failedmessages/failedmessages.css
new file mode 100644
index 0000000000..3ee09db756
--- /dev/null
+++ b/frontend/src/components/failedmessages/failedmessages.css
@@ -0,0 +1,90 @@
+.panel-retry {
+ background-color: #1a1a1a;
+ border: none;
+ color: #fff;
+}
+
+.panel-retry p.lead {
+ color: #fff;
+}
+
+.panel-retry span.metadata,
+.panel-retry sp-moment {
+ color: #b0b5b5 !important;
+}
+
+div.retry-completed.bulk-retry-progress-status {
+ color: #fff;
+ font-weight: bold;
+}
+
+.progress.bulk-retry-progress {
+ margin-bottom: 0;
+ background-color: #333333;
+}
+
+.retry-completed,
+ul.retry-request-progress button {
+ display: inline-block;
+}
+
+ul.retry-request-progress button {
+ background-color: var(--sp-blue);
+}
+
+ul.retry-request-progress li > div {
+ margin-bottom: 6px;
+}
+
+.btn.btn-sm {
+ color: var(--sp-blue);
+ font-size: 14px;
+ font-weight: bold;
+ padding: 0 36px 10px 0;
+}
+
+.panel {
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
+}
+
+.panel-body {
+ padding: 15px;
+}
+
+.panel-body ul {
+ list-style: none;
+ padding-left: 0;
+}
+
+.panel-body ul {
+ list-style: none;
+}
+
+.op-metadata {
+ border-top: 1px solid #414242;
+ padding-top: 15px;
+}
+
+.note {
+ margin-bottom: 10px;
+ background-color: #fcf8e3;
+ border: 1px solid #faebcc;
+ padding: 10px 15px;
+}
+
+.metadata.danger,
+.metadata.danger > .danger {
+ font-weight: normal !important;
+}
+
+.icon {
+ color: var(--reduced-emphasis);
+}
+
+button .icon {
+ margin-right: 2px;
+}
diff --git a/frontend/src/components/failedmessages/messageGroupClient.ts b/frontend/src/components/failedmessages/messageGroupClient.ts
new file mode 100644
index 0000000000..f39f870a08
--- /dev/null
+++ b/frontend/src/components/failedmessages/messageGroupClient.ts
@@ -0,0 +1,68 @@
+import { ServiceControlStore, useServiceControlStore } from "@/stores/ServiceControlStore";
+import type GroupOperation from "@/resources/GroupOperation";
+
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
+export interface SuccessResponse {}
+export interface ErrorResponse {
+ message: string;
+}
+
+class MessageGroupClient {
+ serviceControlStore: ServiceControlStore;
+ constructor() {
+ //this module is only called from within view setup or other pinia stores, so this call is lifecycle safe
+ this.serviceControlStore = useServiceControlStore();
+ }
+
+ public async getExceptionGroups(classifier: string = "") {
+ const [, data] = await this.serviceControlStore.fetchTypedFromServiceControl(`recoverability/groups/${classifier}`);
+ return data;
+ }
+
+ public async getExceptionGroupsForEndpoint(classifier: string = "", classiferFilter = "") {
+ const [, data] = await this.serviceControlStore.fetchTypedFromServiceControl(`recoverability/groups/${classifier}?classifierFilter=${classiferFilter}`);
+ return data;
+ }
+
+ //delete note by group id
+ public async deleteNote(groupId: string) {
+ return this.evaluateResponse(await this.serviceControlStore.deleteFromServiceControl(`recoverability/groups/${groupId}/comment`));
+ }
+
+ //edit or create note by group id
+ public async editOrCreateNote(groupId: string, comment: string) {
+ return this.evaluateResponse(await this.serviceControlStore.postToServiceControl(`recoverability/groups/${groupId}/comment?comment=${comment}`));
+ }
+
+ //archive exception group by group id
+ //archiveGroup
+ public async archiveExceptionGroup(groupId: string) {
+ return this.evaluateResponse(await this.serviceControlStore.postToServiceControl(`recoverability/groups/${groupId}/errors/archive`));
+ }
+
+ //restore group by group id
+ public async restoreGroup(groupId: string) {
+ return this.evaluateResponse(await this.serviceControlStore.postToServiceControl(`recoverability/groups/${groupId}/errors/unarchive`));
+ }
+
+ //retry exception group by group id
+ //retryGroup
+ public async retryExceptionGroup(groupId: string) {
+ return this.evaluateResponse(await this.serviceControlStore.postToServiceControl(`recoverability/groups/${groupId}/errors/retry`));
+ }
+
+ //acknowledge archive exception group by group id
+ public async acknowledgeArchiveGroup(groupId: string) {
+ return this.evaluateResponse(await this.serviceControlStore.deleteFromServiceControl(`recoverability/unacknowledgedgroups/${groupId}`));
+ }
+
+ evaluateResponse(response: Response): SuccessResponse | ErrorResponse {
+ return response.ok ? ({} as SuccessResponse) : ({ message: response.statusText } as ErrorResponse);
+ }
+
+ public isError(obj: SuccessResponse | ErrorResponse): obj is ErrorResponse {
+ return (obj as ErrorResponse).message !== undefined;
+ }
+}
+
+export default () => new MessageGroupClient();
diff --git a/frontend/src/components/getSortFunction.ts b/frontend/src/components/getSortFunction.ts
new file mode 100644
index 0000000000..047b9fbbaf
--- /dev/null
+++ b/frontend/src/components/getSortFunction.ts
@@ -0,0 +1,19 @@
+import { type GroupPropertyType, SortDirection } from "@/resources/SortOptions";
+
+export default function getSortFunction(selector: ((group: T) => GroupPropertyType) | undefined, dir: SortDirection) {
+ if (!selector) {
+ return () => 0;
+ }
+ const sortFunc = (firstElement: T, secondElement: T) => {
+ const x = selector(firstElement);
+ const y = selector(secondElement);
+ if (x > y) {
+ return 1;
+ } else if (x < y) {
+ return -1;
+ }
+ return 0;
+ };
+
+ return dir === SortDirection.Ascending ? sortFunc : (firstElement: T, secondElement: T) => -sortFunc(firstElement, secondElement);
+}
diff --git a/frontend/src/components/heartbeats/EndpointInstances.vue b/frontend/src/components/heartbeats/EndpointInstances.vue
new file mode 100644
index 0000000000..536e60caf4
--- /dev/null
+++ b/frontend/src/components/heartbeats/EndpointInstances.vue
@@ -0,0 +1,253 @@
+
+
+
+
+
+
+
+
+
+ Back
+
{{ endpointName }} Instances
+
+
+
+
+
+
+
+
+ Mute Alerts on All
+
+
+
+ Unmute Alerts on All
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Mute an instance when you are planning to take the instance offline to do maintenance or some other reason. This will prevent alerts on the dashboard.
+
+
+
+
+ Delete
+ Delete an instance when that instance has been decommissioned.
+
+
+
+
+
+
+ You may
+ Delete
+ this endpoint
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ instance.host_display_name }}
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/heartbeats/EndpointSettingsSupported.vue b/frontend/src/components/heartbeats/EndpointSettingsSupported.vue
new file mode 100644
index 0000000000..b846a91a06
--- /dev/null
+++ b/frontend/src/components/heartbeats/EndpointSettingsSupported.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+ The minimum version of ServiceControl required to enable configuring tracking of endpoints is
+ {{ minimumSCVersionForEndpointSettings }} .
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/heartbeats/HealthyEndpoints.vue b/frontend/src/components/heartbeats/HealthyEndpoints.vue
new file mode 100644
index 0000000000..a2c14cd182
--- /dev/null
+++ b/frontend/src/components/heartbeats/HealthyEndpoints.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/heartbeats/HeartbeatConfiguration.vue b/frontend/src/components/heartbeats/HeartbeatConfiguration.vue
new file mode 100644
index 0000000000..eb3a6fe1fe
--- /dev/null
+++ b/frontend/src/components/heartbeats/HeartbeatConfiguration.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Track Instances on All Endpoints
+
+
+
+ Do Not Track Instances on All Endpoints
+
+
+
+
+
+
+
+
+
+
+
+
+
Track Instances by default on new endpoints
+
+
+
+
+
+ If most of your endpoints are auto-scaled, consider changing this setting.
+ If most of your endpoint are hosted in physical infrastructure, consider changing this setting.
+
+
Track Instances is the best setting for endpoints where all instances are hosted in physical infrastructure that is not auto-scaled. Example, physical or virtual servers.
+
Do Not Track Instances is the best setting for endpoints that are hosted in infrastructure with autoscalers. Example, Kubernetes, Azure Container Apps and AWS Elastic Container Service.
+
+
+
+
+
+
+
diff --git a/frontend/src/components/heartbeats/HeartbeatsDashboardItem.vue b/frontend/src/components/heartbeats/HeartbeatsDashboardItem.vue
new file mode 100644
index 0000000000..ae574a696a
--- /dev/null
+++ b/frontend/src/components/heartbeats/HeartbeatsDashboardItem.vue
@@ -0,0 +1,12 @@
+
+
+
+ Heartbeats
+
diff --git a/frontend/src/components/heartbeats/HeartbeatsList.vue b/frontend/src/components/heartbeats/HeartbeatsList.vue
new file mode 100644
index 0000000000..0a41e0b148
--- /dev/null
+++ b/frontend/src/components/heartbeats/HeartbeatsList.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tracking all instances
+ {{ endpoint.alive_count }} alive
+ {{ endpoint.down_count }} no heartbeat
+
+
+ Not tracking instances
+ {{ endpoint.alive_count }} alive
+
+
+
+ {{ store.instanceDisplayText(endpoint) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ endpoint.muted_count }}
+
+
+
+
+
+ {{ endpoint.muted_count }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/heartbeats/HeartbeatsMenuItem.vue b/frontend/src/components/heartbeats/HeartbeatsMenuItem.vue
new file mode 100644
index 0000000000..abf27da5a9
--- /dev/null
+++ b/frontend/src/components/heartbeats/HeartbeatsMenuItem.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Heartbeats
+ {{ failedHeartbeatsCount }}
+
+
+
+
diff --git a/frontend/src/components/heartbeats/LastHeartbeat.vue b/frontend/src/components/heartbeats/LastHeartbeat.vue
new file mode 100644
index 0000000000..f34ff30a44
--- /dev/null
+++ b/frontend/src/components/heartbeats/LastHeartbeat.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ No heartbeat data received for this {{ tooltipTarget }}.
+ Have you installed and configured the heartbeats plugin for this {{ tooltipTarget }}?
+
+ No data available
+
+
+
diff --git a/frontend/src/components/heartbeats/UnhealthyEndpoints.vue b/frontend/src/components/heartbeats/UnhealthyEndpoints.vue
new file mode 100644
index 0000000000..78b18cd894
--- /dev/null
+++ b/frontend/src/components/heartbeats/UnhealthyEndpoints.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/heartbeats/heartbeats.css b/frontend/src/components/heartbeats/heartbeats.css
new file mode 100644
index 0000000000..05f8b75ffd
--- /dev/null
+++ b/frontend/src/components/heartbeats/heartbeats.css
@@ -0,0 +1,91 @@
+.lead.endpoint-details-link.righ-side-ellipsis a {
+ color: #00729c;
+ margin: 0;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ border-bottom: 1px dotted lightgrey;
+}
+
+.lead.endpoint-details-link.righ-side-ellipsis a:hover {
+ border-bottom: 1px solid #00729c;
+ text-decoration: none !important;
+}
+
+.righ-side-ellipsis {
+ direction: rtl;
+ text-align: left;
+}
+
+.box-header {
+ display: flex;
+ gap: 0.5em;
+ max-width: 100%;
+}
+
+.endpoint-name > div > div > a {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: #00729c;
+ border-bottom: 1px dotted lightgrey;
+}
+
+.endpoint-name,
+.endpoint-name > div {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.endpoint-name {
+ gap: 0.25em;
+}
+
+.endpoint-name .box-header {
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ max-width: 100%;
+}
+
+.endpoint-name .box-header > *:not(:first-child) {
+ margin-left: 0.25em;
+}
+
+.endpoint-count {
+ font-weight: bold;
+}
+
+p:not(.lead) {
+ margin: 0 0 5px;
+}
+
+div[role="columnheader"] {
+ display: flex;
+}
+
+div[role="cell"] {
+ padding: 10px;
+ align-items: center;
+ display: flex;
+ gap: 0.25em;
+}
+
+div[role="cell"].centre,
+div[role="columnheader"].centre {
+ justify-content: center;
+}
+
+.grid-row {
+ display: flex;
+ position: relative;
+ border-top: 1px solid #eee;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #eee;
+ border-left: 1px solid #fff;
+ background-color: #fff;
+ margin: 0;
+}
diff --git a/frontend/src/components/heartbeats/isEndpointSettingsSupported.ts b/frontend/src/components/heartbeats/isEndpointSettingsSupported.ts
new file mode 100644
index 0000000000..1eea96802a
--- /dev/null
+++ b/frontend/src/components/heartbeats/isEndpointSettingsSupported.ts
@@ -0,0 +1,8 @@
+import useEnvironmentAndVersionsAutoRefresh from "@/composables/useEnvironmentAndVersionsAutoRefresh";
+
+export const minimumSCVersionForEndpointSettings = "5.9.0";
+
+export default function useIsEndpointSettingsSupported() {
+ const { store: environmentStore } = useEnvironmentAndVersionsAutoRefresh();
+ return environmentStore.serviceControlIsGreaterThan(minimumSCVersionForEndpointSettings);
+}
diff --git a/frontend/src/components/heartbeats/serviceControlWithHeartbeats.ts b/frontend/src/components/heartbeats/serviceControlWithHeartbeats.ts
new file mode 100644
index 0000000000..0f3e82b871
--- /dev/null
+++ b/frontend/src/components/heartbeats/serviceControlWithHeartbeats.ts
@@ -0,0 +1,14 @@
+import { SetupFactoryOptions } from "../../../test/driver";
+import * as precondition from "../../../test/preconditions";
+import { minimumSCVersionForEndpointSettings } from "@/components/heartbeats/isEndpointSettingsSupported";
+
+export const serviceControlWithHeartbeats = async ({ driver }: SetupFactoryOptions) => {
+ await driver.setUp(precondition.hasUpToDateServicePulse);
+ await driver.setUp(precondition.hasUpToDateServiceControl);
+ await driver.setUp(precondition.errorsDefaultHandler);
+ await driver.setUp(precondition.hasCustomChecksEmpty);
+ await driver.setUp(precondition.hasEventLogItems);
+ await driver.setUp(precondition.hasServiceControlMainInstance(minimumSCVersionForEndpointSettings));
+ await driver.setUp(precondition.hasNoDisconnectedEndpoints);
+ await driver.setUp(precondition.hasServiceControlMonitoringInstance);
+};
diff --git a/frontend/src/components/list.css b/frontend/src/components/list.css
new file mode 100644
index 0000000000..fb52cf246e
--- /dev/null
+++ b/frontend/src/components/list.css
@@ -0,0 +1,15 @@
+.lead,
+.box p .lead {
+ word-wrap: break-word;
+ color: #181919;
+ font-size: 1em;
+ font-weight: bold;
+ margin-bottom: 0.2em;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+a {
+ cursor: pointer;
+}
diff --git a/frontend/src/components/messages/BodyView.vue b/frontend/src/components/messages/BodyView.vue
new file mode 100644
index 0000000000..ceb1e8d8bb
--- /dev/null
+++ b/frontend/src/components/messages/BodyView.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
Could not find the message body. This could be because the message URL is invalid or the corresponding message was processed and is no longer tracked by ServiceControl.
+
Message body unavailable.
+
+
+
+
Message body cannot be displayed because content type "{{ bodyState.data.content_type }}" is not supported.
+
+
+
+
diff --git a/frontend/src/components/messages/DeleteMessageButton.vue b/frontend/src/components/messages/DeleteMessageButton.vue
new file mode 100644
index 0000000000..033d39ba1d
--- /dev/null
+++ b/frontend/src/components/messages/DeleteMessageButton.vue
@@ -0,0 +1,44 @@
+
+
+
+
+ Delete message
+
+
+
+
+
diff --git a/frontend/src/components/messages/DiffContent.vue b/frontend/src/components/messages/DiffContent.vue
new file mode 100644
index 0000000000..253569365b
--- /dev/null
+++ b/frontend/src/components/messages/DiffContent.vue
@@ -0,0 +1,401 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ `⟨ Expand ${item.count} lines... ⟩` }}
+
+
+
+
+
+ {{ item.lineInfo.left.lineNumber }}
+ {{ item.lineInfo.left.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ `⟨ Expand ${item.count} lines... ⟩` }}
+
+
+
+
+
+ {{ item.lineInfo.right.lineNumber }}
+ {{ item.lineInfo.right.value }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/DiffViewer.vue b/frontend/src/components/messages/DiffViewer.vue
new file mode 100644
index 0000000000..3048c4e3a1
--- /dev/null
+++ b/frontend/src/components/messages/DiffViewer.vue
@@ -0,0 +1,221 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/EditAndRetryButton.vue b/frontend/src/components/messages/EditAndRetryButton.vue
new file mode 100644
index 0000000000..4976aedf4f
--- /dev/null
+++ b/frontend/src/components/messages/EditAndRetryButton.vue
@@ -0,0 +1,54 @@
+
+
+
+
+ Edit & retry
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/ExportMessageButton.vue b/frontend/src/components/messages/ExportMessageButton.vue
new file mode 100644
index 0000000000..bf88ef7d51
--- /dev/null
+++ b/frontend/src/components/messages/ExportMessageButton.vue
@@ -0,0 +1,30 @@
+
+
+
+ Export message
+
diff --git a/frontend/src/components/messages/FlowDiagram/FlowDiagram.vue b/frontend/src/components/messages/FlowDiagram/FlowDiagram.vue
new file mode 100644
index 0000000000..473cc575bf
--- /dev/null
+++ b/frontend/src/components/messages/FlowDiagram/FlowDiagram.vue
@@ -0,0 +1,449 @@
+
+
+
+
+
FlowDiagram data is unavailable.
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/FlowDiagram/useLayout.ts b/frontend/src/components/messages/FlowDiagram/useLayout.ts
new file mode 100644
index 0000000000..7b76a90dfe
--- /dev/null
+++ b/frontend/src/components/messages/FlowDiagram/useLayout.ts
@@ -0,0 +1,48 @@
+import dagre from "@dagrejs/dagre";
+import { DefaultEdge, Node, Position, useVueFlow } from "@vue-flow/core";
+import { ref } from "vue";
+
+export function useLayout() {
+ const { findNode } = useVueFlow();
+
+ const graph = ref(new dagre.graphlib.Graph());
+
+ function layout(nodes: Node[], edges: DefaultEdge[]) {
+ // we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
+ const dagreGraph = new dagre.graphlib.Graph();
+
+ graph.value = dagreGraph;
+
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
+
+ const isHorizontal = false;
+ dagreGraph.setGraph({ rankdir: "TB" });
+
+ for (const node of nodes) {
+ const graphNode = findNode(node.id);
+ if (graphNode === undefined) continue;
+
+ dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 250, height: graphNode.dimensions.height || 55 });
+ }
+
+ for (const edge of edges) {
+ dagreGraph.setEdge(edge.source, edge.target);
+ }
+
+ dagre.layout(dagreGraph);
+
+ // set nodes with updated positions
+ return nodes.map((node) => {
+ const nodeWithPosition = dagreGraph.node(node.id);
+
+ return {
+ ...node,
+ targetPosition: isHorizontal ? Position.Left : Position.Top,
+ sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
+ position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
+ };
+ });
+ }
+
+ return { graph, layout };
+}
diff --git a/frontend/src/components/messages/HeadersView.vue b/frontend/src/components/messages/HeadersView.vue
new file mode 100644
index 0000000000..7f8c05e7df
--- /dev/null
+++ b/frontend/src/components/messages/HeadersView.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+ No headers found matching the search term.
+ Could not find message headers. This could be because the message URL is invalid or the corresponding message was processed and is no longer tracked by ServiceControl.
+
+
+
diff --git a/frontend/src/components/messages/MessageView.vue b/frontend/src/components/messages/MessageView.vue
new file mode 100644
index 0000000000..b25a2a2a81
--- /dev/null
+++ b/frontend/src/components/messages/MessageView.vue
@@ -0,0 +1,222 @@
+
+
+
+
+
+
+
+
+
+
+
Back
+
+
{{ state.data.message_type }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ View previous version
+
+
+ Failed:
+ Processed at:
+
+ Endpoint: {{ state.data.receiving_endpoint.name }}
+ Machine: {{ state.data.receiving_endpoint.host }}
+
+ Redirect: {{ state.data.failure_metadata.redirect }}
+
+ Deleted:
+ Scheduled for permanent deletion: immediately
+ Scheduled for permanent deletion:
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/MetadataLabel.vue b/frontend/src/components/messages/MetadataLabel.vue
new file mode 100644
index 0000000000..3531068151
--- /dev/null
+++ b/frontend/src/components/messages/MetadataLabel.vue
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/frontend/src/components/messages/RestoreMessageButton.vue b/frontend/src/components/messages/RestoreMessageButton.vue
new file mode 100644
index 0000000000..5390b3f822
--- /dev/null
+++ b/frontend/src/components/messages/RestoreMessageButton.vue
@@ -0,0 +1,41 @@
+
+
+
+
+ Restore
+
+
+
+
+
diff --git a/frontend/src/components/messages/RetryMessageButton.vue b/frontend/src/components/messages/RetryMessageButton.vue
new file mode 100644
index 0000000000..b670a89084
--- /dev/null
+++ b/frontend/src/components/messages/RetryMessageButton.vue
@@ -0,0 +1,38 @@
+
+
+
+
+ Retry message
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram.spec.ts b/frontend/src/components/messages/SagaDiagram.spec.ts
new file mode 100644
index 0000000000..640bb04c4d
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram.spec.ts
@@ -0,0 +1,402 @@
+import { render, describe, test, screen, expect, within } from "@component-test-utils";
+import sut from "@/components/messages/SagaDiagram.vue";
+import { SagaHistory } from "@/resources/SagaHistory";
+import makeRouter from "@/router";
+import { createTestingPinia } from "@pinia/testing";
+import { MessageStore } from "@/stores/MessageStore";
+import { MessageStatus } from "@/resources/Message";
+
+//Defines a domain-specific language (DSL) for interacting with the system under test (sut)
+interface componentDSL {
+ action1(value: string): void;
+ assert: componentDSLAssertions;
+}
+
+//Defines a domain-specific language (DSL) for checking assertions against the system under test (sut)
+interface componentDSLAssertions {
+ thereAreTheFollowingSagaChangesInThisOrder(expectedDatesInOrder: Date[]): void;
+ displayedSagaGuidIs(sagaId: string): void;
+ displayedSagaNameIs(humanizedSagaName: string): void;
+ linkIsShown(arg0: { withText: string; withHref: string }): void;
+ NoSagaDataAvailableMessageIsShownWithMessage(message: RegExp): void;
+ SagaPlugInNeededIsShownWithTheMessages({ messages, withPluginDownloadUrl }: { messages: RegExp[]; withPluginDownloadUrl: string }): void;
+ SagaSequenceIsNotShown(): void;
+}
+
+describe("Feature: Message not involved in Saga", () => {
+ describe("Rule: When the selected message has not participated in a Saga, display a legend indicating it.", () => {
+ test("EXAMPLE: A message that has not participated in a saga is selected", () => {
+ const messageStore = {} as MessageStore;
+ messageStore.state = {} as MessageStore["state"];
+ messageStore.state.data = {} as MessageStore["state"]["data"];
+ messageStore.state.data.invoked_saga = {
+ has_saga: false,
+ saga_id: undefined,
+ saga_type: undefined,
+ };
+
+ // No need to manually set up the store - it will be empty by default
+ const componentDriver = rendercomponent({
+ initialState: {
+ MessageStore: messageStore,
+ SagaDiagramStore: undefined, // Lets pass undefined to simulate no saga data available
+ },
+ });
+
+ componentDriver.assert.NoSagaDataAvailableMessageIsShownWithMessage(/This message is not part of any saga/i);
+ });
+ });
+});
+
+describe("Feature: Detecting no Audited Saga Data Available", () => {
+ describe("Rule: When a message participates in a Saga, but the Saga data is unavailable, display a legend indicating that the Saga audit plugin is needed to visualize the saga.", () => {
+ test("EXAMPLE: A message that was participated in a Saga without the Saga audit plugin being active gets selected", () => {
+ const messageStore = {} as MessageStore;
+ messageStore.state = {} as MessageStore["state"];
+ messageStore.state.data = {} as MessageStore["state"]["data"];
+ messageStore.state.data.invoked_saga = {
+ has_saga: true,
+ saga_id: "saga-id-123",
+ saga_type: "Shipping.ShipOrderWorkflow",
+ };
+
+ // No need to manually set up the store - it will be empty by default
+ const componentDriver = rendercomponent({
+ initialState: {
+ MessageStore: messageStore,
+ SagaDiagramStore: undefined, // Lets pass undefined to simulate no saga data available
+ },
+ });
+
+ componentDriver.assert.SagaPlugInNeededIsShownWithTheMessages({
+ messages: [/Saga audit plugin needed to visualize saga/i, /To visualize your saga, please install the appropriate nuget package in your endpoint/i, /install-package NServiceBus\.SagaAudit/i],
+ withPluginDownloadUrl: "https://www.nuget.org/packages/NServiceBus.SagaAudit",
+ });
+ });
+ });
+});
+
+describe("Feature: Navigation and Contextual Information", () => {
+ describe("Rule: Clearly indicate contextual information like Saga ID and Saga Type.", () => {
+ test("EXAMPLE: A message with a Saga Id '123' and a Saga Type 'ServiceControl.SmokeTest.AuditingSaga' gets selected", () => {
+ const messageStore = {} as MessageStore;
+ messageStore.state = {} as MessageStore["state"];
+ messageStore.state.data = {} as MessageStore["state"]["data"];
+ messageStore.state.data.invoked_saga = {
+ has_saga: true,
+ saga_id: "123",
+ saga_type: "ServiceControl.SmokeTest.AuditingSaga",
+ };
+
+ // Set initial state with sample saga history
+ const componentDriver = rendercomponent({
+ initialState: {
+ MessageStore: messageStore,
+ SagaDiagramStore: { sagaHistory: sampleSagaHistory },
+ },
+ });
+
+ componentDriver.assert.displayedSagaNameIs("AuditingSaga");
+ componentDriver.assert.displayedSagaGuidIs("123");
+ });
+ });
+});
+
+describe("Feature: 3 Visual Representation of Saga Timeline", () => {
+ describe("Rule: 3.1 Clearly indicate the initiation and completion of a saga.", () => {
+ test.todo("EXAMPLE: A message with a Saga Id '123' and a Saga Type 'ServiceControl.SmokeTest.AuditingSaga' gets selected", () => {
+ //"Saga Initiated" is explicitly displayed first, and "Saga Completed" is explicitly displayed at the bottom.
+ });
+ });
+
+ describe("Rule: 3.3 Display a chronological timeline of saga events localized to user environment.", () => {
+ test.each([
+ {
+ timezone: "UTC",
+ },
+ {
+ timezone: "America/Los_Angeles",
+ },
+ ])("EXAMPLE: Rendering a Saga with 4 changes - User Timezone $timezone", ({ timezone }) => {
+ // Each saga event ("Saga Initiated," "Saga Updated," "Timeout Invoked," "Saga Completed") is timestamped to represent progression over time. Events are ordered by the time they ocurred.
+ //TODO: "Incoming messages are displayed on the left, and outgoing messages are displayed on the right." in another test?
+
+ //arragement
+ //sampleSagaHistory already not sorted TODO: Make this more clear so the reader of this test doesn't have to go arround and figure out the preconditions
+ const messageStore = {} as MessageStore;
+ messageStore.state = {} as MessageStore["state"];
+ messageStore.state.data = {} as MessageStore["state"]["data"];
+ messageStore.state.data.invoked_saga = {
+ has_saga: true,
+ saga_id: "123",
+ saga_type: "ServiceControl.SmokeTest.AuditingSaga",
+ };
+
+ // Set the environment to a fixed timezone
+ // JSDOM, used by Vitest, defaults to UTC timezone
+ // To ensure consistency, explicitly set the timezone
+ // This ensures that the rendered local time of the saga changes
+ // will always be interpreted and displayed in the specified timezone, avoiding flakiness
+ process.env.TZ = timezone;
+
+ //access each of the saga changes and update its start time and finish time to the same values being read from the variable declaration,
+ // but set them again explicitly here
+ //so that the reader of this test can see the preconditions at play
+ //and understand the test better without having to jump around
+
+ const startTimeA = new Date("2025-03-28T03:04:08.000Z");
+ const finishTimeA1 = new Date("2025-03-28T03:04:08.000Z");
+ const startTimeB = new Date("2025-03-28T03:04:07.000Z");
+ const finishTimeB1 = new Date("2025-03-28T03:04:07.000Z");
+ const startTimeC = new Date("2025-03-28T03:04:06.000Z");
+ const finishTimeC1 = new Date("2025-03-28T03:04:06.000Z");
+ const startTimeD = new Date("2025-03-28T03:04:05.000Z");
+ const finishTimeD1 = new Date("2025-03-28T03:04:05.000Z");
+
+ sampleSagaHistory.changes[0].start_time = startTimeA;
+ sampleSagaHistory.changes[0].finish_time = finishTimeA1;
+ sampleSagaHistory.changes[1].start_time = startTimeB;
+ sampleSagaHistory.changes[1].finish_time = finishTimeB1;
+ sampleSagaHistory.changes[2].start_time = startTimeC;
+ sampleSagaHistory.changes[2].finish_time = finishTimeC1;
+ sampleSagaHistory.changes[3].start_time = startTimeD;
+ sampleSagaHistory.changes[3].finish_time = finishTimeD1;
+ sampleSagaHistory.changes[3].status = "new";
+
+ //B(1), C(2), A(0), D(3)
+ //B(1), C1(2), C(2), A1(0)
+
+ // Set up the store with sample saga history
+ const componentDriver = rendercomponent({
+ initialState: {
+ MessageStore: messageStore,
+ SagaDiagramStore: { sagaHistory: sampleSagaHistory },
+ },
+ });
+
+ //assert
+ componentDriver.assert.thereAreTheFollowingSagaChangesInThisOrder([startTimeD, startTimeC, startTimeB, startTimeA]);
+ });
+ });
+});
+
+function rendercomponent({ initialState = {} }: { initialState?: { MessageStore?: MessageStore; SagaDiagramStore?: { sagaHistory: SagaHistory } } }): componentDSL {
+ const router = makeRouter();
+
+ // Render with createTestingPinia
+ render(sut, {
+ global: {
+ plugins: [
+ router,
+ createTestingPinia({
+ initialState,
+ stubActions: true, // Explicitly stub actions (this is the default)
+ }),
+ ],
+ stubs: {
+ CodeEditor: true,
+ CopyToClipboard: true,
+ },
+ directives: {
+ // Add stub for tippy directive
+ tippy: () => {},
+ },
+ },
+ });
+
+ const dslAPI: componentDSL = {
+ action1: () => {
+ // Add actions here;
+ },
+ assert: {
+ NoSagaDataAvailableMessageIsShownWithMessage(message: RegExp) {
+ //ensure that the only one status message is shown
+ expect(screen.queryAllByRole("status")).toHaveLength(1);
+
+ const status = screen.queryByRole("status", { name: /message-not-involved-in-saga/i });
+ expect(status).toBeInTheDocument();
+ const statusText = within(status!).getByText(message);
+ expect(statusText).toBeInTheDocument();
+
+ this.SagaSequenceIsNotShown();
+ },
+ SagaPlugInNeededIsShownWithTheMessages({ messages, withPluginDownloadUrl }: { messages: RegExp[]; withPluginDownloadUrl: string }) {
+ // Use the matcher to find the container element
+ const messageContainer = screen.queryByRole("status", { name: /saga-plugin-needed/i });
+ expect(messageContainer).toBeInTheDocument();
+
+ // using within to find the text inside the container per each item in messages
+ messages.forEach((message) => {
+ const statusText = within(messageContainer!).getByText(message);
+ expect(statusText).toBeInTheDocument();
+ });
+
+ // Verify the link
+ const link = screen.getByRole("link", { name: "install-package NServiceBus.SagaAudit" });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute("href", withPluginDownloadUrl);
+
+ this.SagaSequenceIsNotShown();
+ },
+ SagaSequenceIsNotShown() {
+ const sagaSequence = screen.queryByRole("list", { name: /saga-sequence-list/i });
+ expect(sagaSequence).not.toBeInTheDocument();
+ },
+ linkIsShown(args: { withText: string; withHref: string }) {
+ const link = screen.getByRole("link", { name: args.withText });
+ expect(link).toBeInTheDocument();
+ expect(link.getAttribute("href")).toBe(args.withHref);
+ },
+ displayedSagaNameIs(name: string) {
+ const sagaName = screen.getByRole("heading", { name: /saga name/i });
+ expect(sagaName).toBeInTheDocument();
+ expect(sagaName).toHaveTextContent(name);
+ },
+ displayedSagaGuidIs(guid: string) {
+ const sagaGuid = screen.getByRole("note", { name: /saga guid/i });
+ expect(sagaGuid).toBeInTheDocument();
+ expect(sagaGuid).toHaveTextContent(guid);
+ },
+ thereAreTheFollowingSagaChangesInThisOrder: function (expectedDatesInOrder: Date[]): void {
+ //Retrive the main parent component that contains the saga changes
+ const sagaChangesContainer = screen.getByRole("table", { name: /saga-sequence-list/i });
+
+ const sagaUpdatesElements = within(sagaChangesContainer).queryAllByRole("row");
+ //from within each sagaUpdatesElements get the values of an element with aria-label="time stamp"
+ //and check if the values are in the same order as the expectedDatesInOrder array passed to this function
+ const sagaUpdatesTimestamps = sagaUpdatesElements.map((item: HTMLElement) => within(item).getByLabelText("time stamp"));
+
+ //expect the number of found sagaUpdatesTimestamps to be the same as the number of expected dates passed to this function
+ expect(sagaUpdatesTimestamps).toHaveLength(expectedDatesInOrder.length);
+
+ const sagaUpdatesTimestampsValues = sagaUpdatesTimestamps.map((item) => item.innerHTML);
+
+ // Verify we have the same number of rendered timestamps as expected dates
+ expect(sagaUpdatesTimestampsValues).toHaveLength(expectedDatesInOrder.length);
+
+ // For each rendered timestamp, verify it matches the expected date at that position
+ // by formatting the expected date the same way and comparing strings
+ expectedDatesInOrder.forEach((expectedDate, index) => {
+ const expectedFormattedString = expectedDate.toLocaleString();
+ expect(sagaUpdatesTimestampsValues[index]).toBe(expectedFormattedString);
+ });
+ },
+ },
+ };
+
+ return dslAPI;
+}
+
+const sampleSagaHistory: SagaHistory = {
+ id: "45f425fc-26ce-163b-4f64-857b889348f3",
+ saga_id: "45f425fc-26ce-163b-4f64-857b889348f3",
+ saga_type: "ServiceControl.SmokeTest.AuditingSaga",
+ changes: [
+ {
+ start_time: new Date("2025-03-28T03:04:08.3819211Z"),
+ finish_time: new Date("2025-03-28T03:04:08.3836Z"),
+ status: "completed",
+ state_after_change: '{"Id":"45f425fc-26ce-163b-4f64-857b889348f3","Originator":null,"OriginalMessageId":"4b9fdea7-d78c-41f0-91ee-b2ae00328f9c"}',
+ initiating_message: {
+ message_id: "876d89bd-7a1f-43f1-b384-b2ae003290e8",
+ is_saga_timeout_message: true,
+ originating_endpoint: "Endpoint1",
+ originating_machine: "mobvm2",
+ time_sent: new Date("2025-03-28T03:04:06.321561Z"),
+ message_type: "ServiceControl.SmokeTest.MyCustomTimeout",
+ intent: "Send",
+ body_url: "body_url",
+ message_status: MessageStatus.Successful,
+ },
+ outgoing_messages: [],
+ endpoint: "Endpoint1",
+ },
+ {
+ start_time: new Date("2025-03-28T03:04:07.5416262Z"),
+ finish_time: new Date("2025-03-28T03:04:07.5509712Z"),
+ status: "updated",
+ state_after_change: '{"Id":"45f425fc-26ce-163b-4f64-857b889348f3","Originator":null,"OriginalMessageId":"4b9fdea7-d78c-41f0-91ee-b2ae00328f9c"}',
+ initiating_message: {
+ message_id: "1308367f-c6a2-418f-9df2-b2ae00328fc9",
+ is_saga_timeout_message: true,
+ originating_endpoint: "Endpoint1",
+ originating_machine: "mobvm2",
+ time_sent: new Date("2025-03-28T03:04:05.37723Z"),
+ message_type: "ServiceControl.SmokeTest.MyCustomTimeout",
+ intent: "Send",
+ body_url: "body_url",
+ message_status: MessageStatus.Successful,
+ },
+ outgoing_messages: [],
+ endpoint: "Endpoint1",
+ },
+ {
+ start_time: new Date("2025-03-28T03:04:06.3088353Z"),
+ finish_time: new Date("2025-03-28T03:04:06.3218175Z"),
+ status: "updated",
+ state_after_change: '{"Id":"45f425fc-26ce-163b-4f64-857b889348f3","Originator":null,"OriginalMessageId":"4b9fdea7-d78c-41f0-91ee-b2ae00328f9c"}',
+ initiating_message: {
+ message_id: "e5bb5304-7892-4d39-96e2-b2ae003290df",
+ is_saga_timeout_message: false,
+ originating_endpoint: "Sender",
+ originating_machine: "mobvm2",
+ time_sent: new Date("2025-03-28T03:04:06.293765Z"),
+ message_type: "ServiceControl.SmokeTest.SagaMessage2",
+ intent: "Send",
+ body_url: "body_url",
+ message_status: MessageStatus.Successful,
+ },
+ outgoing_messages: [
+ {
+ delivery_delay: "00:00:02",
+ destination: "Endpoint1",
+ message_id: "876d89bd-7a1f-43f1-b384-b2ae003290e8",
+ time_sent: new Date("2025-03-28T03:04:06.3214397Z"),
+ message_type: "ServiceControl.SmokeTest.MyCustomTimeout",
+ intent: "Send",
+ deliver_at: new Date("2025-03-28T03:04:06.293765Z"),
+ is_saga_timeout_message: false,
+ originating_endpoint: "Sender",
+ originating_machine: "mobvm2",
+ body_url: "body_url",
+ message_status: MessageStatus.Successful,
+ },
+ ],
+ endpoint: "Endpoint1",
+ },
+ {
+ start_time: new Date("2025-03-28T03:04:05.3332078Z"),
+ finish_time: new Date("2025-03-28T03:04:05.3799483Z"),
+ status: "new",
+ state_after_change: '{"Id":"45f425fc-26ce-163b-4f64-857b889348f3","Originator":null,"OriginalMessageId":"4b9fdea7-d78c-41f0-91ee-b2ae00328f9c"}',
+ initiating_message: {
+ message_id: "4b9fdea7-d78c-41f0-91ee-b2ae00328f9c",
+ is_saga_timeout_message: false,
+ originating_endpoint: "Sender",
+ originating_machine: "mobvm2",
+ time_sent: new Date("2025-03-28T03:04:05.235534Z"),
+ message_type: "ServiceControl.SmokeTest.SagaMessage1",
+ intent: "Send",
+ body_url: "body_url",
+ message_status: MessageStatus.Successful,
+ },
+ outgoing_messages: [
+ {
+ delivery_delay: "00:00:02",
+ destination: "Endpoint1",
+ message_id: "1308367f-c6a2-418f-9df2-b2ae00328fc9",
+ time_sent: new Date("2025-03-28T03:04:05.3715034Z"),
+ message_type: "ServiceControl.SmokeTest.MyCustomTimeout",
+ intent: "Send",
+ deliver_at: new Date("2025-03-28T03:04:06.293765Z"),
+ is_saga_timeout_message: false,
+ originating_endpoint: "Sender",
+ originating_machine: "mobvm2",
+ body_url: "body_url",
+ message_status: MessageStatus.Successful,
+ },
+ ],
+ endpoint: "Endpoint1",
+ },
+ ],
+};
diff --git a/frontend/src/components/messages/SagaDiagram.vue b/frontend/src/components/messages/SagaDiagram.vue
new file mode 100644
index 0000000000..fc5299693e
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+ {{ vm.ShowMessageData ? "Hide Message Data" : "Show Message Data" }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/MessageDataBox.vue b/frontend/src/components/messages/SagaDiagram/MessageDataBox.vue
new file mode 100644
index 0000000000..4124fe83f7
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/MessageDataBox.vue
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+ Failed to load message data, there might be a connection issue or the message may no longer be available.
+
+
+ No message body data available
+
+
+
+
+
+ Message body cannot be displayed because content type "{{ messageData.body.data.content_type }}" is not supported.
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/NoSagaData.vue b/frontend/src/components/messages/SagaDiagram/NoSagaData.vue
new file mode 100644
index 0000000000..f53ea14477
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/NoSagaData.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
This message is not part of any saga
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/SagaCompletedNode.vue b/frontend/src/components/messages/SagaDiagram/SagaCompletedNode.vue
new file mode 100644
index 0000000000..00ea2a4668
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaCompletedNode.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
Saga Completed
+
{{ completionTime }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/SagaDiagramParser.ts b/frontend/src/components/messages/SagaDiagram/SagaDiagramParser.ts
new file mode 100644
index 0000000000..89cedd2659
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaDiagramParser.ts
@@ -0,0 +1,181 @@
+import { SagaHistory } from "@/resources/SagaHistory";
+import { typeToName } from "@/composables/typeHumanizer";
+import { SagaMessageData } from "@/stores/SagaDiagramStore";
+import { getTimeoutFriendly } from "@/composables/deliveryDelayParser";
+
+export interface SagaMessageViewModel {
+ MessageId: string;
+ FriendlyTypeName: string;
+ FormattedTimeSent: string;
+ Data: SagaMessageData;
+ IsEventMessage: boolean;
+ IsCommandMessage: boolean;
+}
+export interface InitiatingMessageViewModel {
+ FriendlyTypeName: string;
+ IsSagaTimeoutMessage: boolean;
+ FormattedMessageTimestamp: string;
+ IsEventMessage: boolean;
+ MessageData: SagaMessageData;
+ HasRelatedTimeoutRequest?: boolean;
+ MessageId: string;
+}
+export interface SagaTimeoutMessageViewModel extends SagaMessageViewModel {
+ TimeoutFriendly: string;
+ HasBeenProcessed: boolean;
+}
+
+export interface SagaUpdateViewModel {
+ MessageId: string;
+ StartTime: Date;
+ FinishTime: Date;
+ FormattedStartTime: string;
+ InitiatingMessage: InitiatingMessageViewModel;
+ Status: string;
+ StatusDisplay: string;
+ HasTimeout: boolean;
+ IsFirstNode: boolean;
+ OutgoingMessages: SagaMessageViewModel[];
+ OutgoingTimeoutMessages: SagaTimeoutMessageViewModel[];
+ HasOutgoingMessages: boolean;
+ HasOutgoingTimeoutMessages: boolean;
+ showUpdatedPropertiesOnly: boolean;
+ stateAfterChange: string;
+ previousStateAfterChange?: string;
+}
+
+export interface SagaViewModel {
+ SagaTitle: string;
+ SagaGuid: string;
+ ParticipatedInSaga: boolean;
+ HasSagaData: boolean;
+ ShowNoPluginActiveLegend: boolean;
+ SagaCompleted: boolean;
+ FormattedCompletionTime: string;
+ SagaUpdates: SagaUpdateViewModel[];
+ ShowMessageData: boolean;
+}
+
+export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: SagaMessageData[]): SagaUpdateViewModel[] {
+ if (!sagaHistory || !sagaHistory.changes || !sagaHistory.changes.length) return [];
+
+ const timeoutMessageIds = new Set();
+ sagaHistory.changes.forEach((update) => {
+ if (update.outgoing_messages) {
+ update.outgoing_messages.forEach((msg) => {
+ const delivery_delay = msg.delivery_delay || "00:00:00";
+ if (delivery_delay && delivery_delay !== "00:00:00") {
+ timeoutMessageIds.add(msg.message_id);
+ }
+ });
+ }
+ });
+
+ const updates = sagaHistory.changes
+ .map((update) => {
+ const startTime = new Date(update.start_time);
+ const finishTime = new Date(update.finish_time);
+ const initiatingMessageTimestamp = new Date(update.initiating_message?.time_sent || Date.now());
+
+ // Find message data for initiating message
+ const initiatingMessageData = update.initiating_message ? findMessageData(messagesData, update.initiating_message.message_id) : createEmptyMessageData();
+
+ // Create common base message objects with shared properties
+ const outgoingMessages = update.outgoing_messages.map((msg) => {
+ const delivery_delay = msg.delivery_delay || "00:00:00";
+ const timeSent = new Date(msg.time_sent);
+ const hasTimeout = !!delivery_delay && delivery_delay !== "00:00:00";
+ const timeoutSeconds = delivery_delay.split(":")[2] || "0";
+ const isEventMessage = msg.intent === "Publish";
+
+ // Find corresponding message data
+ const messageData = findMessageData(messagesData, msg.message_id);
+ return {
+ MessageType: msg.message_type || "",
+ MessageId: msg.message_id,
+ FormattedTimeSent: timeSent.toLocaleString(),
+ HasTimeout: hasTimeout,
+ TimeoutSeconds: timeoutSeconds,
+ TimeoutFriendly: getTimeoutFriendly(delivery_delay),
+ FriendlyTypeName: typeToName(msg.message_type || ""),
+ Data: messageData,
+ IsEventMessage: isEventMessage,
+ IsCommandMessage: !isEventMessage,
+ };
+ });
+
+ const outgoingTimeoutMessages = outgoingMessages
+ .filter((msg) => msg.HasTimeout)
+ .map((msg) => {
+ // Check if this timeout message has been processed by checking if there's an initiating message with matching ID
+ const hasBeenProcessed = sagaHistory.changes.some((update) => update.initiating_message?.message_id === msg.MessageId);
+
+ return {
+ ...msg,
+ TimeoutFriendly: `${msg.TimeoutFriendly}`,
+ HasBeenProcessed: hasBeenProcessed,
+ } as SagaTimeoutMessageViewModel;
+ });
+
+ const regularMessages = outgoingMessages.filter((msg) => !msg.HasTimeout) as SagaMessageViewModel[];
+
+ const hasTimeout = outgoingTimeoutMessages.length > 0;
+
+ // Check if initiating message is a timeout and if so, if it has a corresponding request in the diagram
+ const hasRelatedTimeoutRequest = update.initiating_message?.is_saga_timeout_message && timeoutMessageIds.has(update.initiating_message?.message_id);
+
+ return {
+ MessageId: update.initiating_message?.message_id || "",
+ StartTime: startTime,
+ FinishTime: finishTime,
+ FormattedStartTime: startTime.toLocaleString(),
+ Status: update.status,
+ StatusDisplay: update.status === "new" ? "Saga Initiated" : "Saga Updated",
+ InitiatingMessage: {
+ FriendlyTypeName: typeToName(update.initiating_message?.message_type || "Unknown Message") || "",
+ MessageId: update.initiating_message?.message_id || "",
+ FormattedMessageTimestamp: initiatingMessageTimestamp.toLocaleString(),
+ MessageData: initiatingMessageData,
+ IsEventMessage: update.initiating_message?.intent === "Publish",
+ IsSagaTimeoutMessage: update.initiating_message?.is_saga_timeout_message || false,
+ HasRelatedTimeoutRequest: hasRelatedTimeoutRequest,
+ },
+ HasTimeout: hasTimeout,
+ IsFirstNode: update.status === "new",
+ OutgoingTimeoutMessages: outgoingTimeoutMessages,
+ OutgoingMessages: regularMessages,
+ HasOutgoingMessages: regularMessages.length > 0,
+ HasOutgoingTimeoutMessages: outgoingTimeoutMessages.length > 0,
+ showUpdatedPropertiesOnly: true, // Default to showing only updated properties
+ stateAfterChange: update.state_after_change || "{}",
+ };
+ })
+ .sort((a, b) => a.StartTime.getTime() - b.StartTime.getTime())
+ .sort((a, b) => a.FinishTime.getTime() - b.FinishTime.getTime());
+
+ // Add reference to previous state for each update except the first one
+ for (let i = 1; i < updates.length; i++) {
+ updates[i].previousStateAfterChange = updates[i - 1].stateAfterChange;
+ }
+
+ return updates;
+}
+
+// Helper function to find message data or create empty data if not found
+function findMessageData(messagesData: SagaMessageData[], messageId: string): SagaMessageData {
+ const messageData = messagesData.find((m) => m.message_id === messageId);
+ return messageData || createEmptyMessageData();
+}
+
+// Helper function to create an empty message data object
+function createEmptyMessageData(): SagaMessageData {
+ return {
+ message_id: "",
+ body: {
+ data: {},
+ loading: false,
+ failed_to_load: false,
+ not_found: false,
+ },
+ };
+}
diff --git a/frontend/src/components/messages/SagaDiagram/SagaHeader.vue b/frontend/src/components/messages/SagaDiagram/SagaHeader.vue
new file mode 100644
index 0000000000..305705edbe
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaHeader.vue
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
{{ sagaTitle }}
+
+ guid {{ sagaGuid }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/SagaOutgoingMessage.vue b/frontend/src/components/messages/SagaDiagram/SagaOutgoingMessage.vue
new file mode 100644
index 0000000000..4a8f36f138
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaOutgoingMessage.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
{{ message.FriendlyTypeName }}
+
{{ message.FormattedTimeSent }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/SagaOutgoingTimeoutMessage.vue b/frontend/src/components/messages/SagaDiagram/SagaOutgoingTimeoutMessage.vue
new file mode 100644
index 0000000000..92d1047130
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaOutgoingTimeoutMessage.vue
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ message.FriendlyTypeName }}
+
{{ message.FormattedTimeSent }}
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/SagaPluginNeeded.vue b/frontend/src/components/messages/SagaDiagram/SagaPluginNeeded.vue
new file mode 100644
index 0000000000..56e2f74923
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaPluginNeeded.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
Saga audit plugin needed to visualize saga
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SagaDiagram/SagaUpdateNode.vue b/frontend/src/components/messages/SagaDiagram/SagaUpdateNode.vue
new file mode 100644
index 0000000000..3b5a038707
--- /dev/null
+++ b/frontend/src/components/messages/SagaDiagram/SagaUpdateNode.vue
@@ -0,0 +1,490 @@
+
+
+
+
+
+
+
+
+
+
{{ update.InitiatingMessage.FriendlyTypeName }}
+
{{ update.InitiatingMessage.FormattedMessageTimestamp }}
+
+
+
+
+
+
+
+ Timeout Invoked
+
+ Timeout Invoked
+
+
+
+
+ {{ update.StatusDisplay }}
+
+
+ {{ update.FormattedStartTime }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Initial Saga State
+
State Changes
+
+
+
+
An error occurred while parsing and displaying the saga state for this update
+
+
+
+
+
+
+
+
+
+
No state changes in this update
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram.vue b/frontend/src/components/messages/SequenceDiagram.vue
new file mode 100644
index 0000000000..cb0606fb11
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
(endpointYOffset = (ev.target as Element).scrollTop)">
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/EndpointTooltip.vue b/frontend/src/components/messages/SequenceDiagram/EndpointTooltip.vue
new file mode 100644
index 0000000000..14b616567f
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/EndpointTooltip.vue
@@ -0,0 +1,35 @@
+
+
+
+
+ Name:
+ {{ endpoint.name }}
+ NSB Version:
+ {{ endpoint.version }}
+ Host:
+ {{ endpoint.host }}
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/EndpointsComponent.vue b/frontend/src/components/messages/SequenceDiagram/EndpointsComponent.vue
new file mode 100644
index 0000000000..10d328a771
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/EndpointsComponent.vue
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+
+
+
+
{{ endpoint.name }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/HandlerTooltip.vue b/frontend/src/components/messages/SequenceDiagram/HandlerTooltip.vue
new file mode 100644
index 0000000000..0f1d525783
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/HandlerTooltip.vue
@@ -0,0 +1,54 @@
+
+
+
+ Start of Conversation
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/HandlersComponent.vue b/frontend/src/components/messages/SequenceDiagram/HandlersComponent.vue
new file mode 100644
index 0000000000..994679f74c
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/HandlersComponent.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+ store.setHighlightId(handler.incomingId)"
+ @mouseleave="() => store.setHighlightId()"
+ @click="handler.incomingId && !handler.messageTypeSelected && store.navigateTo(handler.messageId.uniqueId, handler.messageId.id, handler.isError)"
+ />
+
+
+
+ store.setHighlightId(handler.incomingId)"
+ @mouseleave="() => store.setHighlightId()"
+ @click="handler.incomingId && !handler.messageTypeSelected && store.navigateTo(handler.messageId.uniqueId, handler.messageId.id, handler.isError)"
+ >
+
+ {{ handler.messageType }}
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/RouteTooltip.vue b/frontend/src/components/messages/SequenceDiagram/RouteTooltip.vue
new file mode 100644
index 0000000000..0d27be4ee2
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/RouteTooltip.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/RoutesComponent.vue b/frontend/src/components/messages/SequenceDiagram/RoutesComponent.vue
new file mode 100644
index 0000000000..d8cd9bcb50
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/RoutesComponent.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ store.setHighlightId(arrow.id)"
+ @mouseleave="() => store.setHighlightId()"
+ @click="!arrow.selected && store.navigateTo(arrow.messageId.uniqueId, arrow.messageId.id, arrow.isHandlerError)"
+ :ref="(el) => arrow.setUIRef(el as SVGElement)"
+ >
+
+
+
+
+
+
+
+
+ {{ arrow.messageType }}
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/TimelineComponent.vue b/frontend/src/components/messages/SequenceDiagram/TimelineComponent.vue
new file mode 100644
index 0000000000..550dc974ab
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/TimelineComponent.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/SequenceDiagram/tooltipOverlay.ts b/frontend/src/components/messages/SequenceDiagram/tooltipOverlay.ts
new file mode 100644
index 0000000000..b72e5f6f58
--- /dev/null
+++ b/frontend/src/components/messages/SequenceDiagram/tooltipOverlay.ts
@@ -0,0 +1,60 @@
+import { useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
+import { storeToRefs } from "pinia";
+import { h, watch } from "vue";
+import { useTippy } from "vue-tippy";
+import EndpointTooltip from "./EndpointTooltip.vue";
+import HandlerTooltip from "./HandlerTooltip.vue";
+import RouteTooltip from "./RouteTooltip.vue";
+import { HandlerState } from "@/resources/SequenceDiagram/Handler";
+
+export default function useTooltips() {
+ const store = useSequenceDiagramStore();
+ const { endpoints, handlers, routes } = storeToRefs(store);
+
+ watch(
+ () => endpoints.value.map((endpoint) => endpoint.uiRef),
+ () =>
+ endpoints.value
+ .filter((endpoint) => endpoint.uiRef)
+ .forEach((endpoint) =>
+ useTippy(endpoint.uiRef, {
+ interactive: true,
+ appendTo: () => document.body,
+ content: h(EndpointTooltip, { endpoint }),
+ placement: "bottom",
+ delay: [800, null],
+ })
+ )
+ );
+
+ watch(
+ () => handlers.value.map((handler) => handler.uiRef),
+ () =>
+ handlers.value
+ .filter((handler) => handler.uiRef && handler.state !== HandlerState.Unknown)
+ .forEach((handler) =>
+ useTippy(handler.uiRef, {
+ interactive: true,
+ appendTo: () => document.body,
+ content: h(HandlerTooltip, { handler }),
+ delay: [800, null],
+ })
+ )
+ );
+
+ watch(
+ () => routes.value.map((route) => route.uiRef),
+ () =>
+ routes.value
+ .filter((route) => route.uiRef && route.fromRoutedMessage)
+ .forEach((route) =>
+ useTippy(route.uiRef, {
+ interactive: true,
+ appendTo: () => document.body,
+ content: h(RouteTooltip, { routedMessage: route.fromRoutedMessage! }),
+ delay: [800, null],
+ maxWidth: 400,
+ })
+ )
+ );
+}
diff --git a/frontend/src/components/messages/StacktraceFormatter.vue b/frontend/src/components/messages/StacktraceFormatter.vue
new file mode 100644
index 0000000000..e1a6180ba9
--- /dev/null
+++ b/frontend/src/components/messages/StacktraceFormatter.vue
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+ {{ line }}
+
+
+ {{ line.spaces }}{{ selectedLanguage.at }}
+
+ {{ line.type }} .{{ line.method }} (
+
+ {{ param.type }} {{ param.name }}
+ ,
+ )
+
+
+ {{ selectedLanguage.in }} {{ line.file }} :{{ selectedLanguage.line }} {{ line.lineNumber }}
+
+
+
+
+
+
+
diff --git a/frontend/src/components/messages/StacktraceView.vue b/frontend/src/components/messages/StacktraceView.vue
new file mode 100644
index 0000000000..d14ecefdf9
--- /dev/null
+++ b/frontend/src/components/messages/StacktraceView.vue
@@ -0,0 +1,43 @@
+
+
+
+ Stacktrace not available.
+
+
+
+
+
diff --git a/frontend/src/components/modal.css b/frontend/src/components/modal.css
new file mode 100644
index 0000000000..ad78821857
--- /dev/null
+++ b/frontend/src/components/modal.css
@@ -0,0 +1,92 @@
+.modal-mask {
+ position: fixed;
+ z-index: 9998;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: table;
+ transition: opacity 0.3s ease;
+}
+
+.modal-wrapper {
+ display: table-cell;
+ vertical-align: middle;
+}
+
+.modal-container {
+ width: 600px;
+ margin: 0px auto;
+ padding: 20px 30px;
+ background-color: #fff;
+ border-radius: 2px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
+ transition: all 0.3s ease;
+}
+
+.modal-header {
+ padding: 15px;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.modal-title {
+ margin: 0;
+ line-height: 1.42857143;
+}
+
+.modal-body {
+ position: relative;
+ padding: 15px;
+ overflow: auto;
+ min-height: 0;
+}
+
+.modal-footer .btn + .btn {
+ margin-bottom: 0;
+ margin-left: 5px;
+}
+
+.modal-footer {
+ padding: 15px;
+ text-align: right;
+ border-top: 1px solid #e5e5e5;
+}
+
+.modal-container {
+ padding: 0;
+ border-radius: 5px;
+}
+
+.modal-container .btn {
+ font-size: 14px;
+}
+
+.modal-content {
+ position: relative;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #999;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ -webkit-box-shadow: 0 3px 9px rgb(0 0 0 / 50%);
+ box-shadow: 0 3px 9px rgb(0 0 0 / 50%);
+ outline: 0;
+ max-height: 95vh;
+}
+
+@media (min-width: 768px) {
+ .modal-dialog {
+ width: 600px;
+ margin: 30px auto;
+ }
+}
+
+.modal-open .modal.modal-msg-editor,
+.modal-open {
+ overflow-y: hidden;
+}
+
+.modal .btn.btn-primary {
+ padding: 8px 20px;
+}
diff --git a/frontend/src/components/monitoring/EndpointBacklog.vue b/frontend/src/components/monitoring/EndpointBacklog.vue
new file mode 100644
index 0000000000..c2a2a49a1f
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointBacklog.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatGraphDecimalFromNumber(endpoint.digest.metrics.queueLength?.latest, 0) }} MSGS
+
+
?
+
+
+
+ {{ formatGraphDecimalFromNumber(endpoint.digest.metrics.queueLength?.average, 0) }} MSGS AVG
+
+
?
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointDetails.vue b/frontend/src/components/monitoring/EndpointDetails.vue
new file mode 100644
index 0000000000..defa033484
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointDetails.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ endpointName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ endpoint.errorCount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointInstances.vue b/frontend/src/components/monitoring/EndpointInstances.vue
new file mode 100644
index 0000000000..597be75261
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointInstances.vue
@@ -0,0 +1,220 @@
+
+
+
+
+
+
+
+
+
+
+ {{ Throughput.tooltip }}
+
+
+ {{ ScheduledRetries.tooltip }}
+
+
+ {{ ProcessingTime.tooltip }}
+
+
+ {{ CriticalTime.tooltip }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointList.vue b/frontend/src/components/monitoring/EndpointList.vue
new file mode 100644
index 0000000000..73cbaeb66d
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointList.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+ {{ QueueLength.tooltip }}
+
+
+ {{ Throughput.tooltip }}
+
+
+ {{ ScheduledRetries.tooltip }}
+
+
+ {{ ProcessingTime.unit }}
+
+
+ {{ CriticalTime.tooltip }}
+
+
+
+
+
+
+ {{ endpointGroup.group }}
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointListRow.vue b/frontend/src/components/monitoring/EndpointListRow.vue
new file mode 100644
index 0000000000..2eff118fc9
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointListRow.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointMessageTypes.vue b/frontend/src/components/monitoring/EndpointMessageTypes.vue
new file mode 100644
index 0000000000..93d610e1b8
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointMessageTypes.vue
@@ -0,0 +1,211 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ Throughput.tooltip }}
+
+
+ {{ ScheduledRetries.tooltip }}
+
+
+ {{ ProcessingTime.tooltip }}
+
+
+ {{ CriticalTime.tooltip }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ messageType.assemblyName + "-" + messageType.assemblyVersion }}
+
+
+ {{ type.assemblyName + "-" + type.assemblyVersion }}
+
+
{{ "Culture=" + messageType.culture }}
+
{{ "PublicKeyToken=" + messageType.publicKeyToken }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointTimings.vue b/frontend/src/components/monitoring/EndpointTimings.vue
new file mode 100644
index 0000000000..644949b3cf
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointTimings.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ latestProcessingTime.value }}
+ {{ latestProcessingTime.unit }}
+
+
?
+
+
+
+ {{ averageProcessingTime.value }}
+ {{ averageProcessingTime.unit }} AVG
+
+
?
+
+
+
+
+
+
+
+
+
+ {{ latestCriticalTime.value }}
+ {{ latestCriticalTime.unit }}
+
+
?
+
+
+
+ {{ averageCriticalTime.value }}
+ {{ averageCriticalTime.unit }} AVG
+
+
?
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/EndpointWorkload.vue b/frontend/src/components/monitoring/EndpointWorkload.vue
new file mode 100644
index 0000000000..2b84e8b564
--- /dev/null
+++ b/frontend/src/components/monitoring/EndpointWorkload.vue
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ formatGraphDecimalFromNumber(endpoint.digest.metrics.throughput?.latest, 2) }} MSGS/S
+
?
+
+
+
{{ formatGraphDecimalFromNumber(endpoint.digest.metrics.throughput?.average, 2) }} MSGS/S AVG
+
?
+
+
+
+
+
+
+
+
+
{{ formatGraphDecimalFromNumber(endpoint.digest.metrics.retries?.latest, 2) }} MSGS/S
+
?
+
+
+
{{ formatGraphDecimalFromNumber(endpoint.digest.metrics.retries?.average, 2) }} MSGS/S AVG
+
?
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/LargeGraph.vue b/frontend/src/components/monitoring/LargeGraph.vue
new file mode 100644
index 0000000000..c787df30e7
--- /dev/null
+++ b/frontend/src/components/monitoring/LargeGraph.vue
@@ -0,0 +1,177 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ tickValue }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringFilter.vue b/frontend/src/components/monitoring/MonitoringFilter.vue
new file mode 100644
index 0000000000..8a1f49a0bb
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringFilter.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringGroupBy.vue b/frontend/src/components/monitoring/MonitoringGroupBy.vue
new file mode 100644
index 0000000000..5233edcb8d
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringGroupBy.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringHead.vue b/frontend/src/components/monitoring/MonitoringHead.vue
new file mode 100644
index 0000000000..ef66dade5b
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringHead.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
Endpoints overview
+
+
+
+
MassTransit endpoints are currently not supported by monitoring functionality and will not show in this view.
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringHistoryPeriod.vue b/frontend/src/components/monitoring/MonitoringHistoryPeriod.vue
new file mode 100644
index 0000000000..c89af83230
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringHistoryPeriod.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringMenuItem.vue b/frontend/src/components/monitoring/MonitoringMenuItem.vue
new file mode 100644
index 0000000000..bf6aa8863b
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringMenuItem.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Monitoring
+ {{ disconnectedEndpointsCount }}
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringNoData.vue b/frontend/src/components/monitoring/MonitoringNoData.vue
new file mode 100644
index 0000000000..4a27cd6647
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringNoData.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
No monitoring data available
+
The monitoring service is active but no data is being returned.
+
This may be due to one or more of the following causes:
+
+ No endpoints running.
+ No endpoints with monitoring plugin enabled.
+ Endpoints sending data to incorrect queue or monitoring server listening.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/MonitoringNotAvailable.vue b/frontend/src/components/monitoring/MonitoringNotAvailable.vue
new file mode 100644
index 0000000000..d59fcc6f2a
--- /dev/null
+++ b/frontend/src/components/monitoring/MonitoringNotAvailable.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
No monitoring connectivity
+
+ Monitoring is not available at {{ monitoringUrl }}
+
+
This may be due to one or more of the following causes:
+
+
+ Monitoring has been disabled
+ Monitoring server is not running.
+ Monitoring server address is incorrect.
+ Monitoring server not accessible. You may need to validate the configured hostname in the ServiceControl Management Utility.
+
+
+
+ View connection details
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/PlotData.ts b/frontend/src/components/monitoring/PlotData.ts
new file mode 100644
index 0000000000..30f703edd1
--- /dev/null
+++ b/frontend/src/components/monitoring/PlotData.ts
@@ -0,0 +1,6 @@
+export interface PlotData {
+ points: number[];
+ average: number;
+}
+
+export type Coordinate = [number, number];
diff --git a/frontend/src/components/monitoring/SmallGraph.vue b/frontend/src/components/monitoring/SmallGraph.vue
new file mode 100644
index 0000000000..61cbc23524
--- /dev/null
+++ b/frontend/src/components/monitoring/SmallGraph.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/components/monitoring/endpoint.css b/frontend/src/components/monitoring/endpoint.css
new file mode 100644
index 0000000000..6b9c947b46
--- /dev/null
+++ b/frontend/src/components/monitoring/endpoint.css
@@ -0,0 +1,459 @@
+.endpoint-status {
+ display: flex;
+ top: 1px;
+ margin-left: 7px;
+ padding-left: 0;
+ align-items: baseline;
+}
+
+.endpoint-status .warning {
+ display: block;
+ position: relative;
+}
+
+.endpoint-status .endpoint-status-icon {
+ font-size: 20px;
+ color: #ce4844;
+}
+
+h1 .endpoint-status .endpoint-status-icon {
+ font-size: 24px;
+}
+
+.endpoint-status .endpoint-status-icon {
+ color: #777f7f;
+}
+
+.endpoint-status .endpoint-status-icon:hover {
+ color: #23527c;
+}
+
+.endpoint-status .badge {
+ position: relative;
+ top: 8px;
+ font-size: 10px;
+ margin-right: 0;
+ left: -10px;
+}
+
+.endpoint-status .endpoint-status-icon {
+ font-size: 20px;
+ margin-left: 6px;
+}
+
+.endpoint-status a {
+ position: relative;
+ top: -4px;
+ padding-left: 5px;
+}
+
+.endpoint-status a:hover {
+ text-decoration: none;
+}
+
+.endpoint-status a[ng-if="endpoint.errorCount"] {
+ top: -11px;
+}
+
+.endpoint-status .badge {
+ position: relative;
+ top: 2px;
+ left: -9px;
+ font-size: 10px;
+}
+
+.endpoint-status .pa-endpoint-lost.endpoint-details,
+.endpoint-status .pa-monitoring-lost.endpoint-details,
+.endpoint-status .pa-endpoint-lost.endpoints-overview,
+.endpoint-status .pa-monitoring-lost.endpoints-overview {
+ display: inline-block;
+ width: 26px;
+ height: 26px;
+ left: 6px;
+ position: relative;
+}
+
+.endpoint-status .pa-monitoring-lost.endpoints-overview,
+.endpoint-status .pa-monitoring-lost.endpoint-details {
+ top: 4px;
+}
+
+.pa-endpoint-lost.endpoints-overview {
+ background-image: url("@/assets/endpoint-lost.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.pa-monitoring-lost.endpoints-overview {
+ background-image: url("@/assets/monitoring-lost.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.pa-endpoint-lost.endpoint-details {
+ background-image: url("@/assets/endpoint-lost.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.pa-monitoring-lost.endpoint-details {
+ background-image: url("@/assets/monitoring-lost.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.pa-warning {
+ background-image: url("@/assets/warning.svg");
+ background-position: center;
+ background-repeat: no-repeat;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ margin-left: 4px;
+ padding-top: 31px;
+}
+
+.message-type-status .pa-warning {
+ margin-top: -3px;
+}
+
+.endpoint-message-types .endpoint-status {
+ margin-top: -8px;
+}
+
+.endpoint-name > div > div > a {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: #00729c;
+ border-bottom: 1px dotted lightgrey;
+}
+
+.endpoint-name > div > div > a:first-child:hover {
+ border-bottom: 1px solid #00729c;
+ text-decoration: none !important;
+}
+
+.endpoint-row {
+ display: flex;
+ position: relative;
+ padding: 2px 0 4px;
+ border-top: 1px solid #eee;
+ border-right: 1px solid #fff;
+ border-bottom: 1px solid #eee;
+ border-left: 1px solid #fff;
+ background-color: #fff;
+ margin: 0px;
+}
+
+.endpoint-name,
+.endpoint-name > div {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ flex-wrap: wrap;
+ justify-content: center;
+}
+
+.endpoint-name {
+ gap: 0.25em;
+}
+
+.endpoint-name .box-header.with-status {
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ max-width: 100%;
+}
+
+.endpoint-name .box-header.with-status > *:not(:first-child) {
+ margin-left: 0.25em;
+}
+
+/* RESPONSIVE TWEAKS */
+
+@media only screen and (min-width: 993px) {
+ .filter-period-menu {
+ margin-right: 0px;
+ }
+}
+
+@media only screen and (max-width: 992px) {
+ .filter-period-menu,
+ .sort-menu {
+ float: left !important;
+ }
+
+ .sort-menu {
+ margin-top: 0;
+ padding-top: 0;
+ }
+}
+
+@media only screen and (max-width: 768px) {
+ .filter-toolbar .input-group {
+ margin-bottom: 6px;
+ }
+
+ .msg-group-menu {
+ float: left !important;
+ margin-top: 0;
+ margin-left: 15px;
+ }
+
+ div.sp-pull-right {
+ display: inline-block;
+ float: none;
+ margin-top: 0px;
+ }
+
+ button.btn.btn-default {
+ margin-left: 0;
+ margin-right: 6px;
+ margin-bottom: 6px;
+ }
+
+ .input-group-btn button.btn.btn-default {
+ margin-right: 0;
+ }
+
+ .no-mobile-side-padding {
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ .tabs h5 {
+ padding-bottom: 4px;
+ margin-bottom: 10px;
+ }
+
+ .navbar-collapse.collapse.in {
+ padding: 0 0 0 16px !important;
+ }
+}
+
+@media only screen and (max-width: 480px) {
+ .sidebar-label {
+ margin: 3px 0 14px;
+ }
+
+ .btn-toolbar .btn,
+ .btn-toolbar .btn-group,
+ .btn-toolbar .input-group {
+ float: left !important;
+ }
+
+ button.btn.btn-default.ng-binding.ng-isolate-scope {
+ margin-right: 4px !important;
+ }
+
+ div.btn-toolbar,
+ div.form-inliner {
+ margin-bottom: 12px;
+ }
+
+ .filter-toolbar,
+ .action-toolbar {
+ margin-top: 2px;
+ }
+
+ div.sp-pull-right {
+ display: inline-block;
+ float: none;
+ margin-top: 4px;
+ }
+
+ .progress.bulk-retry-progress {
+ margin-top: 6px;
+ }
+}
+
+@media only screen and (max-width: 320px) {
+ div.btn-toolbar,
+ div.form-inliner {
+ margin-bottom: 0px;
+ }
+}
+
+.table-head-row .col-xl-7,
+.endpoint-row .col-xl-7 {
+ width: 20%;
+}
+
+.table-head-row .col-xl-8,
+.endpoint-row .col-xl-8 {
+ width: 36%;
+}
+
+.table-head-row .col-xl-1,
+.endpoint-row .col-xl-1 {
+ width: 16%;
+}
+
+@media only screen and (min-width: 1730px) {
+ .table-head-row .col-xl-7,
+ .endpoint-row .col-xl-7 {
+ width: 30%;
+ }
+
+ .table-head-row .col-xl-8,
+ .endpoint-row .col-xl-8 {
+ width: 44%;
+ }
+
+ .table-head-row .col-xl-1,
+ .endpoint-row .col-xl-1 {
+ width: 14%;
+ }
+}
+
+.sparkline-value {
+ top: 16px;
+ left: -0.6em;
+ position: relative;
+ font-weight: normal;
+ float: right;
+ width: 25%;
+}
+
+.sparkline-value span {
+ color: #777f7f;
+ text-transform: uppercase;
+ font-size: 11px;
+}
+
+.sparkline-value.sec {
+ color: #0000ff;
+}
+
+.sparkline-value.sec span {
+ color: #007aff;
+}
+
+.sparkline-value.min {
+ color: #8b00d0;
+}
+
+.sparkline-value.min span {
+ color: #b14ae4;
+}
+
+.sparkline-value.hr {
+ color: #d601da;
+}
+
+.sparkline-value.hr span {
+ color: #d764d9;
+}
+
+.sparkline-value.d {
+ color: #ad0017;
+}
+
+.sparkline-value.d span {
+ color: #ff0004;
+}
+
+.graph-message-retries-throughputs,
+.graph-critical-processing-times {
+ margin-left: 0.5%;
+}
+
+.monitoring-no-data {
+ margin: 60px auto 120px;
+ max-width: 520px;
+ line-height: 26px;
+}
+
+.monitoring-no-data h1 {
+ font-size: 30px;
+}
+
+.monitoring-no-data-content {
+ width: 80%;
+ display: inline-block;
+ margin: 0 auto;
+ padding: 3px;
+}
+.monitoring-no-data-list {
+ text-align: left;
+ list-style-type: circle;
+}
+.monitoring-no-data p {
+ font-size: 16px;
+ margin-bottom: 20px;
+ margin-top: -18px;
+}
+
+.monitoring-no-data ul {
+ padding-left: 0;
+ text-align: left;
+ font-size: 16px;
+ margin-bottom: 30px;
+}
+
+.monitoring-no-data .btn {
+ font-size: 16px;
+}
+
+.monitoring-no-data a.btn.btn-default.btn-secondary {
+ margin-left: 10px;
+}
+
+:deep(span.table-header-unit) {
+ color: #777f7f;
+}
+
+.box-header {
+ padding-bottom: 3px;
+ padding-top: 2px;
+}
+
+.box-header ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+
+.righ-side-ellipsis {
+ direction: rtl;
+ text-align: left;
+}
+
+@supports (-ms-ime-align: auto) {
+ .righ-side-ellipsis {
+ direction: ltr;
+ }
+}
+
+@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
+ .righ-side-ellipsis {
+ direction: ltr;
+ }
+}
+
+i.fa.pa-endpoint-lost.endpoints-overview,
+i.fa.pa-monitoring-lost.endpoints-overview,
+i.fa.pa-endpoint-lost.endpoint-details,
+i.fa.pa-monitoring-lost.endpoint-details {
+ position: relative;
+ margin-right: 4px;
+ top: 4px;
+}
+
+i.fa.pa-endpoint-lost.endpoints-overview {
+ top: 8px;
+}
+
+.endpoint-instances .fa.pa-endpoint-lost.endpoint-details {
+ top: 4px;
+}
+
+div[content="Unable to connect to instance"],
+div[content="Unable to connect to monitoring server"] {
+ z-index: 99999;
+}
+
+.pull-left {
+ float: left;
+}
diff --git a/frontend/src/components/monitoring/endpointSubTab.css b/frontend/src/components/monitoring/endpointSubTab.css
new file mode 100644
index 0000000000..022c995410
--- /dev/null
+++ b/frontend/src/components/monitoring/endpointSubTab.css
@@ -0,0 +1,51 @@
+.endpoint-row {
+ padding: 0.5em 1.5em;
+ margin: 0;
+}
+
+.box {
+ box-shadow: none;
+ margin: 0;
+}
+
+.box-no-click {
+ background: none;
+ border-bottom: 1px solid #ced6d3;
+ border-left: none;
+ border-right: none;
+ cursor: default;
+}
+
+.box-no-click:first-child {
+ border-top: 1px solid #ced6d3;
+}
+
+.box-no-click {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.box-no-click:hover {
+ background: none !important;
+ border-bottom: 1px solid #ced6d3 !important;
+ border-left: none;
+ border-right: none;
+ border-top: none;
+ cursor: default;
+ padding-top: 21px;
+}
+
+.box-no-click:first-child:hover {
+ border-top: 1px solid #ced6d3 !important;
+ padding-top: 20px;
+}
+
+.box-no-click > div > div {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.box-no-click > div > div > div {
+ padding-left: 0;
+ padding-right: 0;
+}
diff --git a/frontend/src/components/monitoring/endpointTables.css b/frontend/src/components/monitoring/endpointTables.css
new file mode 100644
index 0000000000..143d50f310
--- /dev/null
+++ b/frontend/src/components/monitoring/endpointTables.css
@@ -0,0 +1,29 @@
+/* Endpoint graph table*/
+
+.table-head-row {
+ display: flex;
+ padding-bottom: 5px;
+ padding-top: 20px;
+ border-bottom: 1px solid #ced6d3;
+ border-top: 1px solid #eee;
+ text-transform: uppercase;
+ color: #181919;
+}
+.table-first-col {
+ width: 20%;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+.table-col {
+ width: 16%;
+}
+
+/*TODO: why should wider viewports give more proportional width to the first column?*/
+@media only screen and (min-width: 1730px) {
+ .table-first-col {
+ width: 30%;
+ }
+ .table-col {
+ width: 14%;
+ }
+}
diff --git a/frontend/src/components/monitoring/endpoints.ts b/frontend/src/components/monitoring/endpoints.ts
new file mode 100644
index 0000000000..f5ae7ae381
--- /dev/null
+++ b/frontend/src/components/monitoring/endpoints.ts
@@ -0,0 +1,40 @@
+import type { DigestValues, EndpointDigest, EndpointMetrics, EndpointValues, EndpointValuesWithTime, ExtendedEndpointDetails } from "@/resources/MonitoringEndpoint";
+
+const defaultMetricData: EndpointValues = {
+ points: [],
+ average: 0,
+};
+
+const defaultTimeMetricData: EndpointValuesWithTime = {
+ ...defaultMetricData,
+ timeAxisValues: [],
+};
+
+export const emptyEndpointMetrics = (): EndpointMetrics => ({
+ queueLength: defaultMetricData,
+ throughput: defaultMetricData,
+ retries: defaultMetricData,
+ processingTime: defaultTimeMetricData,
+ criticalTime: defaultTimeMetricData,
+});
+
+const defaultDigestValuesData: DigestValues = {};
+
+export const emptyEndpointDigest = (): EndpointDigest => ({
+ queueLength: defaultDigestValuesData,
+ throughput: defaultDigestValuesData,
+ retries: defaultDigestValuesData,
+ processingTime: defaultDigestValuesData,
+ criticalTime: defaultDigestValuesData,
+});
+
+export const emptyEndpointDetails = (): ExtendedEndpointDetails => ({
+ instances: [],
+ digest: { metrics: emptyEndpointDigest() },
+ metricDetails: { metrics: emptyEndpointMetrics() },
+ isScMonitoringDisconnected: false,
+ serviceControlId: "",
+ errorCount: 0,
+ isStale: false,
+ messageTypes: [],
+});
diff --git a/frontend/src/components/monitoring/formatGraph.ts b/frontend/src/components/monitoring/formatGraph.ts
new file mode 100644
index 0000000000..81d70a1f70
--- /dev/null
+++ b/frontend/src/components/monitoring/formatGraph.ts
@@ -0,0 +1,42 @@
+import { type EndpointValues } from "@/resources/MonitoringEndpoint";
+import { useFormatTime, useFormatLargeNumber, type ValueWithUnit } from "../../composables/formatter";
+
+export function formatGraphDuration(input?: EndpointValues): ValueWithUnit {
+ if (input != null) {
+ const lastValue = input.points.length > 0 ? input.points[input.points.length - 1] : 0;
+ return useFormatTime(lastValue);
+ }
+ return { value: "0", unit: "" };
+}
+
+export function formatGraphDecimalFromNumber(input?: number, deci?: number): string {
+ input = input ?? 0;
+ let decimals = 0;
+ if (input < 10 || input > 1000000) {
+ decimals = 2;
+ }
+ return useFormatLargeNumber(input, deci || decimals);
+}
+
+export function formatGraphDecimal(input?: EndpointValues, deci?: number): string {
+ input = input ?? {
+ points: [],
+ average: 0,
+ };
+ const lastValue = input.points.length > 0 ? input.points[input.points.length - 1] : 0;
+ return formatGraphDecimalFromNumber(lastValue, deci);
+}
+
+export const largeGraphsMinimumYAxis = Object.freeze({
+ queueLength: 10,
+ throughputRetries: 10,
+ processingCritical: 10,
+});
+
+export const smallGraphsMinimumYAxis = Object.freeze({
+ queueLength: 10,
+ throughput: 10,
+ retries: 10,
+ processingTime: 10,
+ criticalTime: 10,
+});
diff --git a/frontend/src/components/monitoring/graphLines.ts b/frontend/src/components/monitoring/graphLines.ts
new file mode 100644
index 0000000000..957dd71442
--- /dev/null
+++ b/frontend/src/components/monitoring/graphLines.ts
@@ -0,0 +1,79 @@
+import { ref, toValue, watchEffect } from "vue";
+import type { Coordinate, PlotData } from "./PlotData";
+
+export function useGraph(plotdata: () => PlotData | undefined, minimumyaxis: () => number | undefined, minPoints = () => 10) {
+ const valuesPath = ref(""),
+ valuesArea = ref(""),
+ maxYaxis = ref(10),
+ average = ref(0),
+ averageLine = ref("");
+
+ const createGraph = () => {
+ const plotData = toValue(plotdata) ?? { points: [], average: 0 };
+ const values = (() => {
+ let result = plotData.points;
+ if (result.length === 0) {
+ result = new Array(toValue(minPoints)).fill(0);
+ }
+ return result;
+ })();
+ const xTick = 100 / (values.length - 1);
+ const coordinates = values.reduce((points: Coordinate[], yValue, i) => [...points, [i * xTick, yValue] as Coordinate], []);
+ valuesPath.value = new Path().startAt(coordinates[0]).followCoordinates(coordinates.slice(1)).toString();
+ valuesArea.value = new Path().startAt([0, 0]).followCoordinates(coordinates).lineTo([100, 0]).close().toString();
+
+ average.value = plotData.average;
+ //TODO: why is this called minimumYaxis when it's only used to determine the maxYaxis?
+ // should the graph actually set the min y value rather than leave it at 0?
+ const minYaxis = toValue(minimumyaxis) ?? 10;
+ const minimumYaxis = !isNaN(minYaxis) ? Number(minYaxis) : 10;
+ maxYaxis.value = Math.max(...[...values, average.value * 1.5, minimumYaxis]);
+
+ averageLine.value = new Path().startAt([0, average.value]).lineTo([100, average.value]).toString();
+ };
+
+ watchEffect(() => createGraph());
+
+ return { valuesPath, valuesArea, maxYaxis, average, averageLine };
+}
+
+class Path {
+ #pathElements: string[] = [];
+ #complete = false;
+
+ startAt([x, y]: Coordinate) {
+ if (this.#pathElements.length > 0) throw new Error("startAt must be the first call on a path");
+ return this.moveTo([x, y]);
+ }
+
+ moveTo([x, y]: Coordinate) {
+ if (this.#complete) throw new Error("Path is already closed");
+ this.#pathElements.push(`M${x} ${y}`);
+ return this;
+ }
+
+ lineTo([x, y]: Coordinate) {
+ if (this.#complete) throw new Error("Path is already closed");
+ this.#pathElements.push(`L${x} ${y}`);
+ return this;
+ }
+
+ followCoordinates(coordinates: Coordinate[]) {
+ for (const c of coordinates) {
+ this.lineTo(c);
+ }
+ return this;
+ }
+
+ close() {
+ if (this.#complete) throw new Error("Path is already closed");
+ if (this.#pathElements.length === 0) throw new Error("Cannot close an empty path");
+ this.#pathElements.push("Z");
+ this.#complete = true;
+ return this;
+ }
+
+ toString() {
+ return this.#pathElements.join(" ");
+ }
+}
diff --git a/frontend/src/components/monitoring/largeGraphs.css b/frontend/src/components/monitoring/largeGraphs.css
new file mode 100644
index 0000000000..83a4190314
--- /dev/null
+++ b/frontend/src/components/monitoring/largeGraphs.css
@@ -0,0 +1,66 @@
+.graph-values {
+ margin-left: 60px;
+ padding-top: 10px;
+ border-top: 3px solid #fff;
+ margin-top: -8.5px;
+ width: calc(100% - 60px);
+ display: flex;
+ justify-content: space-between;
+}
+
+.metric-digest-value {
+ font-weight: bold;
+ font-size: 22px;
+}
+
+.metric-digest-value div {
+ display: inline-block;
+}
+
+.metric-digest-value-suffix {
+ font-weight: normal;
+ font-size: 14px;
+ display: inline-block;
+ text-transform: uppercase;
+ text-wrap: nowrap;
+}
+
+.metric-digest {
+ padding: 1em;
+}
+
+.metric-digest-header {
+ text-transform: uppercase;
+ display: inline-block;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.current,
+.average {
+ margin-top: 4px;
+ margin-bottom: 8px;
+ padding-left: 4px;
+ line-height: 20px;
+ height: 19px;
+}
+
+.current {
+ border-left: 2.5px solid;
+}
+
+.average {
+ border-left: 1px dashed;
+ padding-left: 6px;
+}
+
+.graph-area {
+ width: 33%;
+ box-sizing: border-box;
+}
+
+@media (max-width: 1300px) {
+ .graph-area {
+ width: 95%;
+ }
+}
diff --git a/frontend/src/components/monitoring/messageTypes.ts b/frontend/src/components/monitoring/messageTypes.ts
new file mode 100644
index 0000000000..7eaa8375de
--- /dev/null
+++ b/frontend/src/components/monitoring/messageTypes.ts
@@ -0,0 +1,58 @@
+import type { ExtendedMessageType, MessageType, MessageTypeDetails } from "@/resources/MonitoringEndpoint";
+
+function shortenTypeName(typeName: string): string {
+ return typeName.split(".").pop() ?? typeName;
+}
+
+function parseTheMessageTypeData(messageType: MessageType): ExtendedMessageType {
+ if (messageType.typeName.indexOf(";") > 0) {
+ const messageTypeHierarchy = messageType.typeName.split(";").map((item) => {
+ const segments = item.split(",");
+ const messageTypeDetails: MessageTypeDetails = {
+ typeName: segments[0],
+ assemblyName: segments[1],
+ assemblyVersion: segments[2].substring(segments[2].indexOf("=") + 1),
+ };
+
+ if (!segments[4]?.endsWith("=null")) {
+ //SC monitoring fills culture only if PublicKeyToken is filled
+ messageTypeDetails.culture = segments[3];
+ messageTypeDetails.publicKeyToken = segments[4];
+ }
+ return messageTypeDetails;
+ });
+ return {
+ ...messageType,
+ messageTypeHierarchy,
+ typeName: messageTypeHierarchy.map((item) => item.typeName).join(", "),
+ shortName: messageTypeHierarchy.map((item) => shortenTypeName(item.typeName)).join(", "),
+ containsTypeHierarchy: true,
+ tooltipText: messageTypeHierarchy.reduce(
+ (sum, item) => (sum ? `${sum}\n ` : "") + `${item.typeName} |${item.assemblyName}-${item.assemblyVersion}` + (item.culture ? ` |${item.culture}` : "") + (item.publicKeyToken ? ` |${item.publicKeyToken}` : ""),
+ ""
+ ),
+ };
+ }
+ const cultureSuffix = messageType.culture && messageType.culture !== "null" ? ` | Culture=${messageType.culture}` : "";
+ const publicKeyTokenSuffix = messageType.publicKeyToken && messageType.publicKeyToken !== "null" ? ` | PublicKeyToken=${messageType.publicKeyToken}` : "";
+
+ return {
+ ...messageType,
+ shortName: shortenTypeName(messageType.typeName),
+ tooltipText: `${messageType.typeName} | ${messageType.assemblyName}-${messageType.assemblyVersion}${cultureSuffix}${publicKeyTokenSuffix}`,
+ };
+}
+
+export default class MessageTypes {
+ totalItems: number;
+ data: ExtendedMessageType[];
+
+ constructor(rawMessageTypes: MessageType[]) {
+ this.totalItems = rawMessageTypes.length;
+ this.data = rawMessageTypes
+ // filter out system message types
+ .filter((mt) => mt.id && mt.typeName)
+ .map((mt) => parseTheMessageTypeData(mt))
+ .sort((a, b) => a.typeName.localeCompare(b.typeName));
+ }
+}
diff --git a/frontend/src/components/monitoring/monitoring.css b/frontend/src/components/monitoring/monitoring.css
new file mode 100644
index 0000000000..f9560daf2d
--- /dev/null
+++ b/frontend/src/components/monitoring/monitoring.css
@@ -0,0 +1,123 @@
+div.avg-tooltip {
+ position: absolute;
+ text-align: left;
+ padding: 0.3rem;
+ line-height: 1;
+ background: var(--avg-tooltip-background-color);
+ color: #ffffff;
+ border-radius: 8px 1px 1px 8px;
+ pointer-events: none;
+ font-size: 11px;
+ white-space: nowrap;
+}
+
+div.avg-tooltip.left {
+ border-radius: 1px 8px 8px 1px;
+}
+
+div.avg-tooltip:before {
+ content: "";
+ display: block;
+ z-index: -1;
+ right: 0;
+ position: absolute;
+ top: 50%;
+ background-color: var(--avg-tooltip-background-color);
+ width: 24px;
+ height: 24px;
+ margin-top: -12px;
+ margin-right: -12px;
+
+ transform: rotate(45deg);
+}
+
+div.avg-tooltip.left:before {
+ right: inherit;
+ margin-right: inherit;
+ margin-left: -12px;
+ left: 0;
+}
+
+div.avg-tooltip .value {
+ font-size: 14px;
+ font-weight: bold;
+}
+
+div.avg-tooltip .value span {
+ font-size: 11px;
+ font-weight: normal;
+}
+
+.queue-length {
+ color: var(--monitoring-queue-length);
+ stroke: var(--monitoring-queue-length);
+}
+
+.queue-length .graph-data-fill {
+ fill: var(--monitoring-queue-length-light);
+ stroke: none;
+}
+
+.queue-length.avg-tooltip {
+ --avg-tooltip-background-color: var(--monitoring-queue-length);
+}
+
+.throughput {
+ color: var(--monitoring-throughput);
+ stroke: var(--monitoring-throughput);
+}
+
+.throughput .graph-data-fill {
+ fill: var(--monitoring-throughput-light);
+ stroke: none;
+}
+
+.throughput.avg-tooltip {
+ --avg-tooltip-background-color: var(--monitoring-throughput);
+}
+
+.retries {
+ color: var(--monitoring-retries);
+ stroke: var(--monitoring-retries);
+}
+
+.retries .graph-data-fill {
+ fill: var(--monitoring-retries-light);
+ stroke: none;
+}
+
+.retries.avg-tooltip {
+ --avg-tooltip-background-color: var(--monitoring-retries);
+}
+
+.processing-time {
+ color: var(--monitoring-processing-time);
+ stroke: var(--monitoring-processing-time);
+}
+
+.processing-time .graph-data-fill {
+ fill: var(--monitoring-processing-time-light);
+ stroke: none;
+}
+
+.processing-time.avg-tooltip {
+ --avg-tooltip-background-color: var(--monitoring-processing-time);
+}
+
+.critical-time {
+ color: var(--monitoring-critical-time);
+ stroke: var(--monitoring-critical-time);
+}
+
+.critical-time .graph-data-fill {
+ fill: var(--monitoring-critical-time-light);
+ stroke: none;
+}
+
+.critical-time.avg-tooltip {
+ --avg-tooltip-background-color: var(--monitoring-critical-time);
+}
+
+.avg-tooltip {
+ color: white;
+}
diff --git a/frontend/src/components/notsupported.css b/frontend/src/components/notsupported.css
new file mode 100644
index 0000000000..43d50b65eb
--- /dev/null
+++ b/frontend/src/components/notsupported.css
@@ -0,0 +1,29 @@
+.not-supported {
+ font-size: 1rem;
+}
+
+.not-supported div:has(> .message) {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.not-supported .message {
+ margin-top: 2em;
+ max-width: 30em;
+ line-height: 1.6em;
+}
+
+.not-supported .message h1 {
+ font-size: 1.9em;
+}
+
+.not-supported .message ul {
+ padding-left: 0;
+ text-align: left;
+ margin-bottom: 1.9rem;
+}
+
+.not-supported .message .btn {
+ font-size: 1rem;
+}
diff --git a/frontend/src/composables/LicenseStatus.ts b/frontend/src/composables/LicenseStatus.ts
new file mode 100644
index 0000000000..47c2e74113
--- /dev/null
+++ b/frontend/src/composables/LicenseStatus.ts
@@ -0,0 +1,5 @@
+export enum LicenseWarningLevel {
+ None = "none",
+ Warning = "warning",
+ Danger = "danger",
+}
diff --git a/frontend/src/composables/autoRefresh.ts b/frontend/src/composables/autoRefresh.ts
new file mode 100644
index 0000000000..eb01dc397d
--- /dev/null
+++ b/frontend/src/composables/autoRefresh.ts
@@ -0,0 +1,66 @@
+import { watch, ref, shallowReadonly, type WatchStopHandle } from "vue";
+import { useCounter, useDocumentVisibility, useTimeoutPoll } from "@vueuse/core";
+
+export default function useFetchWithAutoRefresh(name: string, fetch: () => Promise, intervalMs: number) {
+ let watchStop: WatchStopHandle | null = null;
+ const { count, inc, dec, reset } = useCounter(0);
+ const interval = ref(intervalMs);
+ const isRefreshing = ref(false);
+ const fetchWrapper = async () => {
+ if (isRefreshing.value) {
+ return;
+ }
+ isRefreshing.value = true;
+ await fetch();
+ isRefreshing.value = false;
+ };
+ const { pause, resume } = useTimeoutPoll(
+ fetchWrapper,
+ interval,
+ { immediate: false, immediateCallback: true } // we control first fetch manually
+ );
+
+ const visibility = useDocumentVisibility();
+
+ const start = async () => {
+ inc();
+ if (count.value === 1) {
+ console.debug(`[AutoRefresh] Starting auto-refresh for ${name} every ${interval.value}ms`);
+ resume();
+ watchStop = watch(visibility, (current, previous) => {
+ if (current === "visible" && previous === "hidden") {
+ console.debug(`[AutoRefresh] Resuming auto-refresh for ${name} as document became visible`);
+ resume();
+ }
+
+ if (current === "hidden" && previous === "visible") {
+ console.debug(`[AutoRefresh] Pausing auto-refresh for ${name} as document became hidden`);
+ pause();
+ }
+ });
+ } else {
+ console.debug(`[AutoRefresh] Incremented refCount for ${name} to ${count.value}`);
+ // Because another component has started using the auto-refresh, do an immediate refresh to ensure it has up-to-date data
+ await fetchWrapper();
+ }
+ };
+
+ const stop = () => {
+ dec();
+ if (count.value <= 0) {
+ console.debug(`[AutoRefresh] Stopping auto-refresh for ${name}`);
+ pause();
+ watchStop?.();
+ watchStop = null;
+ reset();
+ } else {
+ console.debug(`[AutoRefresh] Decremented refCount for ${name} to ${count.value}`);
+ }
+ };
+
+ const updateInterval = (newIntervalMs: number) => {
+ interval.value = newIntervalMs;
+ };
+
+ return { refreshNow: fetchWrapper, isRefreshing: shallowReadonly(isRefreshing), updateInterval, start, stop };
+}
diff --git a/frontend/src/composables/contentTypeParser.ts b/frontend/src/composables/contentTypeParser.ts
new file mode 100644
index 0000000000..2805fdcdf9
--- /dev/null
+++ b/frontend/src/composables/contentTypeParser.ts
@@ -0,0 +1,63 @@
+import { CodeLanguage } from "@/components/codeEditorTypes";
+
+function parseContentType(contentType: string | undefined): { isSupported: boolean; language?: CodeLanguage } {
+ if (contentType === undefined) {
+ return {
+ isSupported: false,
+ };
+ }
+
+ // remove content type parameter, e.g. charset=utf-8
+ contentType = contentType.split(";")[0].trim();
+
+ if (contentType === "application/json") {
+ return {
+ isSupported: true,
+ language: "json",
+ };
+ }
+
+ if (contentType === "text/xml") {
+ return {
+ isSupported: true,
+ language: "xml",
+ };
+ }
+
+ if (contentType.startsWith("text/")) {
+ return {
+ isSupported: true,
+ };
+ }
+
+ if (contentType === "application/xml") {
+ return {
+ isSupported: true,
+ language: "xml",
+ };
+ }
+
+ if (contentType.startsWith("application/")) {
+ // Some examples:
+ // application/atom+xml
+ // application/ld+json
+ // application/vnd.masstransit+json
+ if (contentType.endsWith("+json")) {
+ return {
+ isSupported: true,
+ language: "json",
+ };
+ } else if (contentType.endsWith("+xml")) {
+ return {
+ isSupported: true,
+ language: "xml",
+ };
+ }
+ }
+
+ return {
+ isSupported: false,
+ };
+}
+
+export default parseContentType;
diff --git a/frontend/src/composables/dateFormatter.ts b/frontend/src/composables/dateFormatter.ts
new file mode 100644
index 0000000000..c0c1fce495
--- /dev/null
+++ b/frontend/src/composables/dateFormatter.ts
@@ -0,0 +1,123 @@
+import moment from "moment";
+import type { DateRange } from "@/types/date";
+
+export interface DateDisplayOptions {
+ showLocalTime?: boolean;
+ showUtcTime?: boolean;
+ showRelative?: boolean;
+ format?: string;
+ emptyText?: string;
+}
+
+/**
+ * Composable for consistent date formatting across the application
+ */
+export function useDateFormatter() {
+ const emptyDate = "0001-01-01T00:00:00";
+
+ /**
+ * Format a date range for display
+ */
+ function formatDateRange(dateRange: DateRange, options: DateDisplayOptions = {}): string {
+ const { emptyText = "No dates" } = options;
+
+ if (dateRange.length === 0) return emptyText;
+
+ const [fromDate, toDate] = dateRange;
+
+ if (toDate && toDate > new Date()) return "Date cannot be in the future";
+ if (fromDate && toDate) return `${fromDate.toLocaleString()} - ${toDate.toLocaleString()}`;
+ if (fromDate) return fromDate.toLocaleString();
+ return emptyText;
+ }
+
+ /**
+ * Format a single date with flexible options
+ */
+ function formatDate(dateInput: string | Date | null, options: DateDisplayOptions = {}): string {
+ const { showLocalTime = true, showUtcTime = false, showRelative = false, format = "LLLL", emptyText = "n/a" } = options;
+
+ if (!dateInput || dateInput === emptyDate) {
+ return emptyText;
+ }
+
+ const m = moment.utc(dateInput);
+
+ if (showRelative) {
+ return m.fromNow();
+ }
+
+ if (showLocalTime && showUtcTime) {
+ return `${m.local().format(format)} (local)\n${m.utc().format(format)} (UTC)`;
+ }
+
+ if (showUtcTime) {
+ return m.utc().format(format);
+ }
+
+ return m.local().format(format);
+ }
+
+ /**
+ * Format date for tooltip display (local and UTC)
+ */
+ function formatDateTooltip(dateInput: string | Date | null, titleValue?: string): string {
+ if (titleValue) return titleValue;
+ if (!dateInput || dateInput === emptyDate) return "";
+
+ const m = moment.utc(dateInput);
+ return `${m.local().format("LLLL")} (local)\n${m.utc().format("LLLL")} (UTC)`;
+ }
+
+ /**
+ * Get relative time that updates periodically
+ */
+ function formatRelativeTime(dateInput: string | Date | null, options: DateDisplayOptions = {}): string {
+ const { emptyText = "n/a" } = options;
+
+ if (!dateInput || dateInput === emptyDate) {
+ return emptyText;
+ }
+
+ return moment.utc(dateInput).fromNow();
+ }
+
+ /**
+ * Format for license expiration dates
+ */
+ function formatLicenseDate(dateInput: string | null): string {
+ if (!dateInput) return "";
+ return new Date(dateInput.replace("Z", "")).toLocaleDateString();
+ }
+
+ /**
+ * Validate if a date range is valid
+ */
+ function isValidDateRange(dateRange: DateRange): boolean {
+ // Empty range is valid
+ if (dateRange.length === 0) return true;
+
+ const [fromDate, toDate] = dateRange;
+
+ // If we have a toDate, it must not be in the future
+ if (toDate && toDate > new Date()) return false;
+
+ // If we have a fromDate but no toDate, that's valid
+ if (fromDate && !toDate) return true;
+
+ // If we have both dates, fromDate should be before or equal to toDate
+ if (fromDate && toDate) return fromDate <= toDate;
+
+ return true;
+ }
+
+ return {
+ formatDate,
+ formatDateRange,
+ formatDateTooltip,
+ formatRelativeTime,
+ formatLicenseDate,
+ isValidDateRange,
+ emptyDate,
+ };
+}
diff --git a/frontend/src/composables/deliveryDelayParser.ts b/frontend/src/composables/deliveryDelayParser.ts
new file mode 100644
index 0000000000..843249952f
--- /dev/null
+++ b/frontend/src/composables/deliveryDelayParser.ts
@@ -0,0 +1,24 @@
+export function parseDeliveryDelay(delay: string): { days: number; hours: number; minutes: number; seconds: number } {
+ // Split on period first to handle multi-digit days
+ const parts = delay.split(".");
+ let days = 0;
+ let timeComponent = delay;
+
+ if (parts.length > 1) {
+ days = parseInt(parts[0], 10);
+ timeComponent = parts[1];
+ }
+
+ const [hours, minutes, seconds] = timeComponent.split(":").map(Number);
+ return { days, hours, minutes, seconds };
+}
+
+function getFriendly(time: number, text: string): string {
+ return time > 0 ? `${time}${text}` : "";
+}
+
+export function getTimeoutFriendly(delivery_delay: string): string {
+ const { days, hours, minutes, seconds } = parseDeliveryDelay(delivery_delay);
+
+ return `${getFriendly(days, "d")}${getFriendly(hours, "h")}${getFriendly(minutes, "m")}${getFriendly(seconds, "s")}`;
+}
diff --git a/frontend/src/composables/fileDownloadCreator.ts b/frontend/src/composables/fileDownloadCreator.ts
new file mode 100644
index 0000000000..5234c571e1
--- /dev/null
+++ b/frontend/src/composables/fileDownloadCreator.ts
@@ -0,0 +1,25 @@
+export function downloadFileFromString(text: string, fileType: string, fileName: string) {
+ const fileBlob = new Blob([text], { type: fileType });
+ const url = URL.createObjectURL(fileBlob);
+ downloadFile(url, fileType, fileName);
+}
+
+export async function downloadFileFromResponse(response: Response, fileType: string, fileName: string) {
+ const fileBlob = await response.blob();
+ const url = URL.createObjectURL(new Blob([fileBlob], { type: fileType }));
+ downloadFile(url, fileType, fileName);
+}
+
+function downloadFile(url: string, fileType: string, fileName: string) {
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", fileName);
+ link.dataset.downloadurl = [fileType, link.download, link.href].join(":");
+ link.style.display = "none";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ setTimeout(() => {
+ URL.revokeObjectURL(link.href);
+ }, 1500);
+}
diff --git a/frontend/src/composables/formatUtils.ts b/frontend/src/composables/formatUtils.ts
new file mode 100644
index 0000000000..60cfa23502
--- /dev/null
+++ b/frontend/src/composables/formatUtils.ts
@@ -0,0 +1,19 @@
+import { useFormatTime } from "@/composables/formatter";
+
+export function formatTypeName(type: string) {
+ const clazz = type.split(",")[0];
+ let objectName = clazz.split(".").pop() ?? "";
+ objectName = objectName.replace("+", ".");
+ return objectName;
+}
+
+export function formatDotNetTimespan(timespan: string) {
+ const time = useFormatTime(dotNetTimespanToMilliseconds(timespan));
+ return `${time.value} ${time.unit}`;
+}
+
+export function dotNetTimespanToMilliseconds(timespan: string) {
+ //assuming if we have days in the timespan then something is very, very wrong
+ const [hh, mm, ss] = timespan.split(":");
+ return ((parseInt(hh) * 60 + parseInt(mm)) * 60 + parseFloat(ss)) * 1000;
+}
diff --git a/frontend/src/composables/formatter.ts b/frontend/src/composables/formatter.ts
new file mode 100644
index 0000000000..8f88a58b87
--- /dev/null
+++ b/frontend/src/composables/formatter.ts
@@ -0,0 +1,67 @@
+import moment from "moment";
+
+const secondDuration = moment.duration(1000);
+const minuteDuration = moment.duration(60 * 1000);
+const hourDuration = moment.duration(60 * 60 * 1000); //this ensures that we never use minute formatting
+const dayDuration = moment.duration(24 * 60 * 60 * 1000);
+
+export interface ValueWithUnit {
+ value: string;
+ unit: string;
+}
+
+export function useFormatTime(value?: number): ValueWithUnit {
+ const time = { value: "0", unit: "ms" };
+ if (value) {
+ const duration = moment.duration(value);
+ if (duration >= dayDuration) {
+ time.value = formatTimeValue(duration.days()) + " d " + formatTimeValue(duration.hours()) + " hrs";
+ } else if (duration >= hourDuration) {
+ time.value = formatTimeValue(duration.hours(), true) + ":" + formatTimeValue(duration.minutes(), true);
+ time.unit = "hr";
+ } else if (duration >= minuteDuration) {
+ time.value = formatTimeValue(duration.minutes()) + ":" + formatTimeValue(duration.seconds());
+ time.unit = "min";
+ } else if (duration >= secondDuration) {
+ time.value = formatTimeValue(duration.seconds());
+ time.unit = "sec";
+ } else {
+ time.value = formatTimeValue(duration.asMilliseconds());
+ time.unit = "ms";
+ }
+ }
+
+ return time;
+}
+
+export function useGetDayDiffFromToday(value: string) {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const diff = new Date(value.replace("Z", "")).getTime() - today.getTime();
+ return Math.round(diff / 1000 / 60 / 60 / 24);
+}
+
+export function useFormatLargeNumber(num: number, decimals: number) {
+ const suffixes = ["k", "M", "G", "T", "P", "E"];
+
+ if (isNaN(num)) {
+ return "";
+ }
+
+ if (num < 1000000) {
+ return round(num, decimals).toLocaleString();
+ }
+
+ const exp = Math.floor(Math.log(num) / Math.log(1000));
+
+ return `${round(num / Math.pow(1000, exp), decimals).toLocaleString()}${suffixes[exp - 1]}`;
+}
+
+function round(num: number, decimals: number) {
+ return Number(num.toFixed(decimals));
+}
+
+function formatTimeValue(timeValue: number, displayTwoDigits = false) {
+ const strValue = Math.floor(timeValue);
+ return `${displayTwoDigits ? ("0" + strValue).slice(-2) : strValue.toLocaleString()}`;
+}
diff --git a/frontend/src/composables/isRouteSelected.ts b/frontend/src/composables/isRouteSelected.ts
new file mode 100644
index 0000000000..e86d665a74
--- /dev/null
+++ b/frontend/src/composables/isRouteSelected.ts
@@ -0,0 +1,8 @@
+import { useLink, useRoute } from "vue-router";
+
+export default function isRouteSelected(path: string) {
+ const route = useRoute();
+ const pathRoute = useLink({ to: path }).route.value;
+
+ return route.matched.some((match) => match.name === pathRoute.name);
+}
diff --git a/frontend/src/composables/serviceSemVer.ts b/frontend/src/composables/serviceSemVer.ts
new file mode 100644
index 0000000000..b1a4b6258a
--- /dev/null
+++ b/frontend/src/composables/serviceSemVer.ts
@@ -0,0 +1,77 @@
+const reSemver = /^v?((\d+)\.(\d+)\.(\d+))(?:-([\dA-Za-z\-_]+(?:\.[\dA-Za-z\-_]+)*))?(?:\+([\dA-Za-z\-_]+(?:\.[\dA-Za-z\-_]+)*))?$/;
+
+export function isUpgradeAvailable(currentVersion: string, latestVersion: string) {
+ const latest = parse(latestVersion.split("-")[0]);
+ const current = parse(currentVersion.split("-")[0]);
+
+ if (latest == null) return false;
+ if (current == null) return false;
+
+ if (latest.major !== current.major) {
+ return latest.major > current.major;
+ }
+ if (latest.minor !== current.minor) {
+ return latest.minor > current.minor;
+ }
+ if (latest.patch !== current.patch) {
+ return latest.patch > current.patch;
+ }
+
+ return false;
+}
+
+export function isSupported(currentVersion: string, minSupportedVersion: string) {
+ const minSupported = parse(minSupportedVersion);
+ const current = parse(currentVersion);
+
+ if (current == null) return false;
+ if (minSupported == null) return true;
+
+ if (minSupported.major !== current.major) {
+ return minSupported.major <= current.major;
+ }
+ if (minSupported.minor !== current.minor) {
+ return minSupported.minor <= current.minor;
+ }
+ if (minSupported.patch !== current.patch) {
+ return minSupported.patch <= current.patch;
+ }
+
+ return true;
+}
+
+interface SemVer {
+ semver: string | null;
+ version: string;
+ major: number;
+ minor: number;
+ patch: number;
+ release: string;
+ build: string;
+}
+
+function parse(version: string) {
+ // semver, major, minor, patch
+ // https://github.com/mojombo/semver/issues/32
+ // https://github.com/isaacs/node-semver/issues/10
+ // optional v
+ const m = reSemver.exec(version) || [];
+
+ function defaultToZero(num: string) {
+ const n = parseInt(num, 10);
+
+ return isNaN(n) ? 0 : n;
+ }
+
+ return 0 === m.length
+ ? null
+ : {
+ semver: m[0],
+ version: m[1],
+ major: defaultToZero(m[2]),
+ minor: defaultToZero(m[3]),
+ patch: defaultToZero(m[4]),
+ release: m[5],
+ build: m[6],
+ };
+}
diff --git a/frontend/src/composables/toast.ts b/frontend/src/composables/toast.ts
new file mode 100644
index 0000000000..847afdcd03
--- /dev/null
+++ b/frontend/src/composables/toast.ts
@@ -0,0 +1,28 @@
+import ToastPopup from "@/components/ToastPopup.vue";
+import { TYPE, useToast } from "vue-toastification";
+import { ToastOptions } from "vue-toastification/dist/types/types";
+
+export function useShowToast(type: TYPE, title: string, message: string, doNotUseTimeout: boolean = false, options?: ToastOptions) {
+ const toast = useToast();
+ const content = {
+ // Your component or JSX template
+ component: ToastPopup,
+
+ // Props are just regular props, but these won't be reactive
+ props: {
+ type: type,
+ title: title,
+ message: message,
+ },
+ };
+ toast(content, {
+ timeout: doNotUseTimeout ? false : undefined,
+ type: type,
+ ...options,
+ });
+}
+
+export const showToastAfterOperation = async (operation: () => Promise, toastType: TYPE, title: string, message: string) => {
+ await operation();
+ useShowToast(toastType, title, message);
+};
diff --git a/frontend/src/composables/typeHumanizer.ts b/frontend/src/composables/typeHumanizer.ts
new file mode 100644
index 0000000000..c311555411
--- /dev/null
+++ b/frontend/src/composables/typeHumanizer.ts
@@ -0,0 +1,11 @@
+export function typeToName(type: string | null | undefined): string | null {
+ if (!type) {
+ return null;
+ }
+
+ const className = type.split(",")[0];
+ let objectName = className.split(".").pop() || "";
+ objectName = objectName.replace(/\+/g, ".");
+
+ return objectName;
+}
diff --git a/frontend/src/composables/useAutoRefresh.ts b/frontend/src/composables/useAutoRefresh.ts
new file mode 100644
index 0000000000..cbc15a0987
--- /dev/null
+++ b/frontend/src/composables/useAutoRefresh.ts
@@ -0,0 +1,41 @@
+import { getCurrentInstance, onMounted, onUnmounted } from "vue";
+import useFetchWithAutoRefresh from "./autoRefresh";
+
+export function useAutoRefresh(name: string, refresh: () => Promise, intervalMs: number) {
+ const { start, stop } = useFetchWithAutoRefresh(name, refresh, intervalMs);
+
+ function useAutoRefresh() {
+ if (!getCurrentInstance()) return; //should only happen in some test contexts. Refresh will need to be called manually for those cases
+ onMounted(start);
+ onUnmounted(stop);
+ }
+
+ return useAutoRefresh;
+}
+
+/**
+ * Creates a singleton auto-refresh composable for a Pinia store.
+ * This handles the timing issue where the store needs to be called within a component lifecycle
+ * but the auto-refresh manager needs to be a singleton.
+ *
+ * @param name - Name for logging purposes
+ * @param useStore - Function that returns the Pinia store (called within component lifecycle)
+ * @param intervalMs - Refresh interval in milliseconds
+ * @returns A composable function that sets up auto-refresh and returns the store
+ */
+export function useStoreAutoRefresh Promise }>(name: string, useStore: () => TStore, intervalMs: number) {
+ const refresh = () => {
+ if (!store) {
+ return Promise.resolve();
+ }
+ return store.refresh();
+ };
+ let store: TStore | null = null;
+ const autoRefresh = useAutoRefresh(name, refresh, intervalMs);
+
+ return () => {
+ store = useStore();
+ autoRefresh();
+ return { store };
+ };
+}
diff --git a/frontend/src/composables/useConnectionsAndStatsAutoRefresh.ts b/frontend/src/composables/useConnectionsAndStatsAutoRefresh.ts
new file mode 100644
index 0000000000..6381fb890b
--- /dev/null
+++ b/frontend/src/composables/useConnectionsAndStatsAutoRefresh.ts
@@ -0,0 +1,4 @@
+import { useConnectionsAndStatsStore } from "@/stores/ConnectionsAndStatsStore";
+import { useStoreAutoRefresh } from "./useAutoRefresh";
+
+export default useStoreAutoRefresh("connectionsAndStats", useConnectionsAndStatsStore, 5000);
diff --git a/frontend/src/composables/useCustomChecksStoreAutoRefresh.ts b/frontend/src/composables/useCustomChecksStoreAutoRefresh.ts
new file mode 100644
index 0000000000..e3eb1f2fce
--- /dev/null
+++ b/frontend/src/composables/useCustomChecksStoreAutoRefresh.ts
@@ -0,0 +1,4 @@
+import { useCustomChecksStore } from "@/stores/CustomChecksStore";
+import { useStoreAutoRefresh } from "./useAutoRefresh";
+
+export default useStoreAutoRefresh("customChecks", useCustomChecksStore, 5000);
diff --git a/frontend/src/composables/useEnvironmentAndVersionsAutoRefresh.ts b/frontend/src/composables/useEnvironmentAndVersionsAutoRefresh.ts
new file mode 100644
index 0000000000..9d25ab99d0
--- /dev/null
+++ b/frontend/src/composables/useEnvironmentAndVersionsAutoRefresh.ts
@@ -0,0 +1,4 @@
+import { useEnvironmentAndVersionsStore } from "@/stores/EnvironmentAndVersionsStore";
+import { useStoreAutoRefresh } from "./useAutoRefresh";
+
+export default useStoreAutoRefresh("environmentAndVersions", useEnvironmentAndVersionsStore, 5000);
diff --git a/frontend/src/composables/useHeartbeatInstancesStoreAutoRefresh.ts b/frontend/src/composables/useHeartbeatInstancesStoreAutoRefresh.ts
new file mode 100644
index 0000000000..9b6c5b45d2
--- /dev/null
+++ b/frontend/src/composables/useHeartbeatInstancesStoreAutoRefresh.ts
@@ -0,0 +1,4 @@
+import { useHeartbeatInstancesStore } from "@/stores/HeartbeatInstancesStore";
+import { useStoreAutoRefresh } from "./useAutoRefresh";
+
+export default useStoreAutoRefresh("heartbeatInstances", useHeartbeatInstancesStore, 5000);
diff --git a/frontend/src/composables/useHeartbeatsStoreAutoRefresh.ts b/frontend/src/composables/useHeartbeatsStoreAutoRefresh.ts
new file mode 100644
index 0000000000..107cbbbe7a
--- /dev/null
+++ b/frontend/src/composables/useHeartbeatsStoreAutoRefresh.ts
@@ -0,0 +1,4 @@
+import { useHeartbeatsStore } from "@/stores/HeartbeatsStore";
+import { useStoreAutoRefresh } from "./useAutoRefresh";
+
+export default useStoreAutoRefresh("heartbeats", useHeartbeatsStore, 5000);
diff --git a/frontend/src/composables/useThroughputStoreAutoRefresh.ts b/frontend/src/composables/useThroughputStoreAutoRefresh.ts
new file mode 100644
index 0000000000..b44aa4e33a
--- /dev/null
+++ b/frontend/src/composables/useThroughputStoreAutoRefresh.ts
@@ -0,0 +1,4 @@
+import { useThroughputStore } from "@/stores/ThroughputStore";
+import { useStoreAutoRefresh } from "./useAutoRefresh";
+
+export default useStoreAutoRefresh("throughput", useThroughputStore, 60 * 60 * 1000 /* 1 hour */);
diff --git a/frontend/src/defaultConfig.ts b/frontend/src/defaultConfig.ts
new file mode 100644
index 0000000000..4ef3ed3165
--- /dev/null
+++ b/frontend/src/defaultConfig.ts
@@ -0,0 +1,20 @@
+export interface DefaultConfig {
+ default_route: string;
+ version: string;
+ service_control_url: string;
+ monitoring_url: string;
+ showPendingRetry: boolean;
+}
+
+let config: DefaultConfig | null = null;
+
+export function setDefaultConfig(defaultConfig: DefaultConfig): void {
+ config = defaultConfig;
+}
+
+export function getDefaultConfig(): DefaultConfig {
+ if (!config) {
+ throw new Error("defaultConfig has not been initialized");
+ }
+ return config;
+}
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
new file mode 100644
index 0000000000..7099ddccdf
--- /dev/null
+++ b/frontend/src/main.ts
@@ -0,0 +1,39 @@
+import makeRouter from "./router";
+import { mount } from "./mount";
+import "vue-toastification/dist/index.css";
+import "vue3-simple-typeahead/dist/vue3-simple-typeahead.css"; //Optional default CSS
+import "./assets/main.css";
+import "tippy.js/dist/tippy.css";
+import { setDefaultConfig } from "./defaultConfig";
+
+async function conditionallyEnableMocking() {
+ if (process.env.NODE_ENV !== "dev-mocks") {
+ return;
+ }
+
+ const { worker } = await import("@/../test/mocks/browser");
+
+ // `worker.start()` returns a Promise that resolves
+ // once the Service Worker is up and ready to intercept requests.
+ return worker.start();
+}
+
+// eslint-disable-next-line promise/catch-or-return
+conditionallyEnableMocking()
+ .then(async () => {
+ const response = await fetch("js/app.constants.json", {
+ method: "GET",
+ });
+
+ // eslint-disable-next-line promise/always-return
+ if (response.ok) {
+ const appConstants = await response.json();
+ setDefaultConfig(appConstants);
+ } else {
+ console.error("Failed to load app constants");
+ }
+ })
+ // eslint-disable-next-line promise/always-return
+ .then(() => {
+ mount({ router: makeRouter() });
+ });
diff --git a/frontend/src/mount.ts b/frontend/src/mount.ts
new file mode 100644
index 0000000000..d8eab93497
--- /dev/null
+++ b/frontend/src/mount.ts
@@ -0,0 +1,34 @@
+import { createApp } from "vue";
+import type { Router } from "vue-router";
+import App from "./App.vue";
+import Toast, { type PluginOptions, POSITION } from "vue-toastification";
+import VueTippy from "vue-tippy";
+import { createPinia } from "pinia";
+import SimpleTypeahead from "vue3-simple-typeahead";
+
+const toastOptions: PluginOptions = {
+ position: POSITION.BOTTOM_RIGHT,
+ timeout: 5000,
+ transition: "Vue-Toastification__fade",
+ hideProgressBar: true,
+ containerClassName: "toast-container",
+ toastClassName: "vue-toast",
+ closeButtonClassName: "toast-close-button",
+};
+
+export function mount({ router }: { router: Router }) {
+ router.beforeEach((to, _from, next) => {
+ document.title = to.meta.title || "ServicePulse";
+ next();
+ });
+
+ const app = createApp(App);
+ app.use(router).use(Toast, toastOptions).use(SimpleTypeahead).use(createPinia()).use(VueTippy);
+ app.mount(`#app`);
+
+ app.config.errorHandler = (err, instance) => {
+ console.error(instance, err);
+ };
+
+ return app;
+}
diff --git a/frontend/src/resources/Configuration.ts b/frontend/src/resources/Configuration.ts
new file mode 100644
index 0000000000..53b2bacf73
--- /dev/null
+++ b/frontend/src/resources/Configuration.ts
@@ -0,0 +1,47 @@
+export default interface Configuration {
+ host: Host;
+ data_retention: DataRetention;
+ performance_tunning: PerformanceTuning;
+ transport: Transport;
+ plugins: Plugins;
+ mass_transit_connector?: MassTransitConnector;
+}
+interface MassTransitConnector {
+ version: string;
+ logs: Array<{ level: string; message: string; date: string }>;
+ error_queues: Array<{ name: string; ingesting: boolean }>;
+}
+interface Plugins {
+ heartbeat_grace_period: string;
+}
+interface Transport {
+ transport_type: string;
+ error_log_queue: string;
+ error_queue: string;
+ forward_error_messages: boolean;
+}
+interface PerformanceTuning {
+ http_default_connection_limit: number;
+ external_integrations_dispatching_batch_size: number;
+ expiration_process_batch_size: number;
+ expiration_process_timer_in_seconds: number;
+}
+interface DataRetention {
+ error_retention_period: string;
+}
+interface Host {
+ service_name: string;
+ raven_db_path: string;
+ logging: Logging;
+}
+interface Logging {
+ log_path: string;
+ logging_level: string;
+ raven_db_log_level: string;
+}
+
+export interface EditAndRetryConfig {
+ enabled: boolean;
+ sensitive_headers: string[];
+ locked_headers: string[];
+}
diff --git a/frontend/src/resources/ConnectionState.ts b/frontend/src/resources/ConnectionState.ts
new file mode 100644
index 0000000000..93936f8032
--- /dev/null
+++ b/frontend/src/resources/ConnectionState.ts
@@ -0,0 +1,6 @@
+export interface ConnectionState {
+ connected: boolean;
+ connecting: boolean;
+ connectedRecently: boolean;
+ unableToConnect: boolean | null;
+}
diff --git a/frontend/src/resources/ConnectionTestResults.ts b/frontend/src/resources/ConnectionTestResults.ts
new file mode 100644
index 0000000000..ea7944c639
--- /dev/null
+++ b/frontend/src/resources/ConnectionTestResults.ts
@@ -0,0 +1,12 @@
+export default interface ConnectionTestResults {
+ transport: string;
+ audit_connection_result: ConnectionSettingsTestResult;
+ monitoring_connection_result: ConnectionSettingsTestResult;
+ broker_connection_result: ConnectionSettingsTestResult;
+}
+
+export interface ConnectionSettingsTestResult {
+ connection_successful: boolean;
+ connection_error_messages: string[];
+ diagnostics: string;
+}
diff --git a/frontend/src/resources/CustomCheck.ts b/frontend/src/resources/CustomCheck.ts
new file mode 100644
index 0000000000..eb9123bc8c
--- /dev/null
+++ b/frontend/src/resources/CustomCheck.ts
@@ -0,0 +1,16 @@
+import type EndpointDetails from "@/resources/EndpointDetails";
+
+export default interface CustomCheck {
+ id: string;
+ custom_check_id: string;
+ category: string;
+ status: Status;
+ reported_at: string;
+ failure_reason: string;
+ originating_endpoint: EndpointDetails;
+}
+
+export enum Status {
+ Fail = "Fail",
+ Pass = "Pass",
+}
diff --git a/frontend/src/resources/EditMessage.ts b/frontend/src/resources/EditMessage.ts
new file mode 100644
index 0000000000..3aade1776c
--- /dev/null
+++ b/frontend/src/resources/EditMessage.ts
@@ -0,0 +1,13 @@
+import type Header from "./Header";
+
+export interface HeaderWithEditing extends Header {
+ isLocked: boolean;
+ isSensitive: boolean;
+ isMarkedAsRemoved: boolean;
+ isChanged: boolean;
+}
+
+export interface EditedMessage {
+ messageBody: string;
+ headers: HeaderWithEditing[];
+}
diff --git a/frontend/src/resources/EditRetryResponse.ts b/frontend/src/resources/EditRetryResponse.ts
new file mode 100644
index 0000000000..4554155860
--- /dev/null
+++ b/frontend/src/resources/EditRetryResponse.ts
@@ -0,0 +1,3 @@
+export default interface EditRetryResponse {
+ edit_ignored: boolean;
+}
diff --git a/frontend/src/resources/EmailNotifications.ts b/frontend/src/resources/EmailNotifications.ts
new file mode 100644
index 0000000000..d80bba6f07
--- /dev/null
+++ b/frontend/src/resources/EmailNotifications.ts
@@ -0,0 +1,10 @@
+export default interface EmailNotifications {
+ enabled: boolean;
+ smtp_server?: string;
+ smtp_port?: number;
+ authentication_account?: string;
+ authentication_password?: string;
+ enable_tls: boolean;
+ to?: string;
+ from?: string;
+}
diff --git a/frontend/src/resources/EndpointDetails.ts b/frontend/src/resources/EndpointDetails.ts
new file mode 100644
index 0000000000..f61aebc7f9
--- /dev/null
+++ b/frontend/src/resources/EndpointDetails.ts
@@ -0,0 +1,5 @@
+export default interface EndpointDetails {
+ name: string;
+ host_id: string;
+ host: string;
+}
diff --git a/frontend/src/resources/EndpointSettings.ts b/frontend/src/resources/EndpointSettings.ts
new file mode 100644
index 0000000000..50f6290cbd
--- /dev/null
+++ b/frontend/src/resources/EndpointSettings.ts
@@ -0,0 +1,4 @@
+export interface EndpointSettings {
+ name: string;
+ track_instances: boolean;
+}
diff --git a/frontend/src/resources/EndpointThroughputSummary.ts b/frontend/src/resources/EndpointThroughputSummary.ts
new file mode 100644
index 0000000000..6cb3f2c9f0
--- /dev/null
+++ b/frontend/src/resources/EndpointThroughputSummary.ts
@@ -0,0 +1,9 @@
+interface EndpointThroughputSummary {
+ name: string;
+ is_known_endpoint: boolean;
+ user_indicator: string;
+ max_daily_throughput: number;
+ max_monthly_throughput?: number;
+}
+
+export default EndpointThroughputSummary;
diff --git a/frontend/src/resources/EndpointView.ts b/frontend/src/resources/EndpointView.ts
new file mode 100644
index 0000000000..e8e83dfe9c
--- /dev/null
+++ b/frontend/src/resources/EndpointView.ts
@@ -0,0 +1,13 @@
+import { EndpointStatus } from "@/resources/Heartbeat";
+
+export interface EndpointsView {
+ id: string;
+ name: string;
+ host_display_name: string;
+ monitor_heartbeat: boolean;
+ heartbeat_information?: {
+ last_report_at: string;
+ reported_status: EndpointStatus;
+ };
+ is_sending_heartbeats: boolean;
+}
diff --git a/frontend/src/resources/EventLogItem.ts b/frontend/src/resources/EventLogItem.ts
new file mode 100644
index 0000000000..02722b12c5
--- /dev/null
+++ b/frontend/src/resources/EventLogItem.ts
@@ -0,0 +1,16 @@
+export default interface EventLogItem {
+ id: string;
+ description: string;
+ severity: Severity;
+ raised_at: string;
+ related_to: string[];
+ category: string;
+ event_type: string;
+}
+
+export enum Severity {
+ Critical = "critical",
+ Error = "error",
+ Warning = "warning",
+ Info = "info",
+}
diff --git a/frontend/src/resources/FailedMessage.ts b/frontend/src/resources/FailedMessage.ts
new file mode 100644
index 0000000000..28748fdfcc
--- /dev/null
+++ b/frontend/src/resources/FailedMessage.ts
@@ -0,0 +1,70 @@
+import type EndpointDetails from "@/resources/EndpointDetails";
+import type Header from "@/resources/Header";
+import { ConversationModel } from "./SequenceDiagram/SequenceModel";
+
+export interface FailedMessage {
+ id: string;
+ message_type: string;
+ time_sent?: string;
+ is_system_message: boolean;
+ exception: ExceptionDetails;
+ message_id: string;
+ number_of_processing_attempts: number;
+ status: FailedMessageStatus;
+ sending_endpoint: EndpointDetails;
+ receiving_endpoint: EndpointDetails;
+ queue_address: string;
+ time_of_failure: string;
+ last_modified: string;
+ edited: boolean;
+ edit_of: string;
+}
+
+export interface ExtendedFailedMessage extends FailedMessage {
+ error_retention_period: number;
+ delete_soon: boolean;
+ deleted_in: string;
+ retryInProgress: boolean;
+ deleteInProgress: boolean;
+ restoreInProgress: boolean;
+ selected: boolean;
+ retried: boolean;
+ archiving: boolean;
+ restoring: boolean;
+ archived: boolean;
+ resolved: boolean;
+ headersNotFound: boolean;
+ messageBodyNotFound: boolean;
+ bodyUnavailable: boolean;
+ headers: Header[];
+ conversationId: string;
+ conversation?: ConversationModel;
+ messageBody: string;
+ contentType: string;
+ isEditAndRetryEnabled: boolean;
+ redirect: boolean;
+ submittedForRetrial: boolean;
+}
+
+export interface FailedMessageError {
+ notFound: boolean;
+ error: boolean;
+}
+
+export function isError(obj: ExtendedFailedMessage | FailedMessageError): obj is FailedMessageError {
+ return (obj as FailedMessageError).error !== undefined || (obj as FailedMessageError).notFound !== undefined;
+}
+
+export interface ExceptionDetails {
+ exception_type: string;
+ message: string;
+ source: string;
+ stack_trace: string;
+}
+
+export enum FailedMessageStatus {
+ Unresolved = "unresolved",
+ Resolved = "resolved",
+ RetryIssued = "retryIssued",
+ Archived = "archived",
+}
diff --git a/frontend/src/resources/FailureGroup.ts b/frontend/src/resources/FailureGroup.ts
new file mode 100644
index 0000000000..094f1e0512
--- /dev/null
+++ b/frontend/src/resources/FailureGroup.ts
@@ -0,0 +1,8 @@
+export default interface FailureGroup {
+ id: string;
+ title: string;
+ type: string;
+ count: number;
+ first: string;
+ last: string;
+}
diff --git a/frontend/src/resources/FailureGroupView.ts b/frontend/src/resources/FailureGroupView.ts
new file mode 100644
index 0000000000..612eb82da5
--- /dev/null
+++ b/frontend/src/resources/FailureGroupView.ts
@@ -0,0 +1,9 @@
+export default interface FailureGroupView {
+ id: string;
+ title: string;
+ type: string;
+ count: number;
+ comment: string;
+ first: string;
+ last: string;
+}
diff --git a/frontend/src/resources/GroupOperation.ts b/frontend/src/resources/GroupOperation.ts
new file mode 100644
index 0000000000..dedca37cfa
--- /dev/null
+++ b/frontend/src/resources/GroupOperation.ts
@@ -0,0 +1,18 @@
+export default interface GroupOperation {
+ id: string;
+ title: string;
+ type: string;
+ count: number;
+ operation_messages_completed_count?: number;
+ comment: string;
+ first?: string;
+ last?: string;
+ operation_status: string;
+ operation_failed?: boolean;
+ operation_progress: number;
+ operation_remaining_count?: number;
+ operation_startTime?: string;
+ operation_completion_time?: string;
+ need_user_acknowledgement: boolean;
+ last_operation_completion_time?: string;
+}
diff --git a/frontend/src/resources/Header.ts b/frontend/src/resources/Header.ts
new file mode 100644
index 0000000000..84d91211e9
--- /dev/null
+++ b/frontend/src/resources/Header.ts
@@ -0,0 +1,56 @@
+export default interface Header {
+ key: NServiceBusHeaders;
+ value?: string;
+}
+
+export enum NServiceBusHeaders {
+ HttpFrom = "NServiceBus.From",
+ HttpTo = "NServiceBus.To",
+ RouteTo = "NServiceBus.Header.RouteTo",
+ DestinationSites = "NServiceBus.DestinationSites",
+ OriginatingSite = "NServiceBus.OriginatingSite",
+ SagaId = "NServiceBus.SagaId",
+ MessageId = "NServiceBus.MessageId",
+ CorrelationId = "NServiceBus.CorrelationId",
+ ReplyToAddress = "NServiceBus.ReplyToAddress",
+ NServiceBusVersion = "NServiceBus.Version",
+ ReturnMessageErrorCodeHeader = "NServiceBus.ReturnMessage.ErrorCode",
+ ControlMessageHeader = "NServiceBus.ControlMessage",
+ SagaType = "NServiceBus.SagaType",
+ OriginatingSagaId = "NServiceBus.OriginatingSagaId",
+ OriginatingSagaType = "NServiceBus.OriginatingSagaType",
+ DelayedRetries = "NServiceBus.Retries",
+ DelayedRetriesTimestamp = "NServiceBus.Retries.Timestamp",
+ ImmediateRetries = "NServiceBus.FLRetries",
+ ProcessingStarted = "NServiceBus.ProcessingStarted",
+ ProcessingEnded = "NServiceBus.ProcessingEnded",
+ TimeSent = "NServiceBus.TimeSent",
+ DeliverAt = "NServiceBus.DeliverAt",
+ RelatedTo = "NServiceBus.RelatedTo",
+ EnclosedMessageTypes = "NServiceBus.EnclosedMessageTypes",
+ ContentType = "NServiceBus.ContentType",
+ SubscriptionMessageType = "SubscriptionMessageType",
+ SubscriberTransportAddress = "NServiceBus.SubscriberAddress",
+ SubscriberEndpoint = "NServiceBus.SubscriberEndpoint",
+ IsSagaTimeoutMessage = "NServiceBus.IsSagaTimeoutMessage",
+ IsDeferredMessage = "NServiceBus.IsDeferredMessage",
+ OriginatingEndpoint = "NServiceBus.OriginatingEndpoint",
+ OriginatingMachine = "NServiceBus.OriginatingMachine",
+ OriginatingHostId = "$.diagnostics.originating.hostid",
+ ProcessingEndpoint = "NServiceBus.ProcessingEndpoint",
+ ProcessingMachine = "NServiceBus.ProcessingMachine",
+ HostDisplayName = "$.diagnostics.hostdisplayname",
+ HostId = "$.diagnostics.hostid",
+ HasLicenseExpired = "$.diagnostics.license.expired",
+ OriginatingAddress = "NServiceBus.OriginatingAddress",
+ ConversationId = "NServiceBus.ConversationId",
+ PreviousConversationId = "NServiceBus.PreviousConversationId",
+ MessageIntent = "NServiceBus.MessageIntent",
+ NonDurableMessage = "NServiceBus.NonDurableMessage",
+ TimeToBeReceived = "NServiceBus.TimeToBeReceived",
+ DiagnosticsTraceParent = "traceparent",
+ DiagnosticsTraceState = "tracestate",
+ DiagnosticsBaggage = "baggage",
+ DataBusConfigContentType = "NServiceBus.DataBusConfig.ContentType",
+ ExceptionInfoExceptionType = "NServiceBus.ExceptionInfo.ExceptionType",
+}
diff --git a/frontend/src/resources/Heartbeat.ts b/frontend/src/resources/Heartbeat.ts
new file mode 100644
index 0000000000..2bbda54f40
--- /dev/null
+++ b/frontend/src/resources/Heartbeat.ts
@@ -0,0 +1,17 @@
+export interface LogicalEndpoint {
+ name: string;
+ monitor_heartbeat: boolean;
+ heartbeat_information?: {
+ last_report_at: string;
+ reported_status: EndpointStatus;
+ };
+ track_instances: boolean;
+ alive_count: number;
+ down_count: number;
+ muted_count: number;
+}
+
+export enum EndpointStatus {
+ Alive = "beating",
+ Dead = "dead",
+}
diff --git a/frontend/src/resources/HistoricRetryOperation.ts b/frontend/src/resources/HistoricRetryOperation.ts
new file mode 100644
index 0000000000..66204ca3fc
--- /dev/null
+++ b/frontend/src/resources/HistoricRetryOperation.ts
@@ -0,0 +1,11 @@
+import { RetryType } from "./RetryType";
+
+export default interface HistoricRetryOperation {
+ request_id: string;
+ retry_type: RetryType;
+ start_time: string;
+ completion_time: string;
+ originator: string;
+ failed: boolean;
+ number_of_messages_processed: number;
+}
diff --git a/frontend/src/resources/LicenseInfo.ts b/frontend/src/resources/LicenseInfo.ts
new file mode 100644
index 0000000000..2ed247bc76
--- /dev/null
+++ b/frontend/src/resources/LicenseInfo.ts
@@ -0,0 +1,37 @@
+import Configuration from "./Configuration";
+
+export default interface LicenseInfo {
+ registered_to: string;
+ edition: string;
+ expiration_date: string;
+ upgrade_protection_expiration: string;
+ license_type: string;
+ instance_name: string;
+ trial_license: boolean;
+ license_status: LicenseStatus;
+ license_extension_url?: string;
+ status: string;
+}
+
+export function typeText(license: LicenseInfo, configuration: Configuration | null) {
+ if (license.trial_license && configuration?.mass_transit_connector) {
+ return "Early Access ";
+ }
+}
+
+export enum LicenseStatus {
+ Valid = "Valid",
+ Unavailable = "Unavailable",
+ InvalidDueToExpiredSubscription = "InvalidDueToExpiredSubscription",
+ ValidWithExpiringTrial = "ValidWithExpiringTrial",
+ InvalidDueToExpiredTrial = "InvalidDueToExpiredTrial",
+ InvalidDueToExpiredUpgradeProtection = "InvalidDueToExpiredUpgradeProtection",
+ ValidWithExpiredUpgradeProtection = "ValidWithExpiredUpgradeProtection",
+ ValidWithExpiringUpgradeProtection = "ValidWithExpiringUpgradeProtection",
+ ValidWithExpiringSubscription = "ValidWithExpiringSubscription",
+}
+export enum LicenseType {
+ Subscription,
+ Trial,
+ UpgradeProtection,
+}
diff --git a/frontend/src/resources/Message.ts b/frontend/src/resources/Message.ts
new file mode 100644
index 0000000000..ed13869609
--- /dev/null
+++ b/frontend/src/resources/Message.ts
@@ -0,0 +1,49 @@
+import type EndpointDetails from "@/resources/EndpointDetails";
+import Header from "./Header";
+
+export default interface Message {
+ id: string;
+ message_id: string;
+ message_type: string;
+ sending_endpoint: EndpointDetails;
+ receiving_endpoint: EndpointDetails;
+ time_sent: string;
+ processed_at: string;
+ critical_time: string;
+ processing_time: string;
+ delivery_time: string;
+ is_system_message: boolean;
+ conversation_id: string;
+ headers: Header[];
+ status: MessageStatus;
+ message_intent: MessageIntent;
+ body_url: string;
+ body_size: number;
+ instance_id: string;
+ invoked_sagas?: SagaInfo[];
+ originates_from_saga?: SagaInfo;
+}
+
+export enum MessageStatus {
+ Failed = "failed",
+ RepeatedFailure = "repeatedFailure",
+ Successful = "successful",
+ ResolvedSuccessfully = "resolvedSuccessfully",
+ ArchivedFailure = "archivedFailure",
+ RetryIssued = "retryIssued",
+}
+
+export enum MessageIntent {
+ Send = "send",
+ Publish = "publish",
+ Subscribe = "subscribe",
+ Unsubscribe = "unsubscribe",
+ Reply = "reply",
+ Init = "init",
+}
+
+export interface SagaInfo {
+ change_status?: string;
+ saga_type: string;
+ saga_id: string;
+}
diff --git a/frontend/src/resources/MonitoredEndpoint.ts b/frontend/src/resources/MonitoredEndpoint.ts
new file mode 100644
index 0000000000..638f828674
--- /dev/null
+++ b/frontend/src/resources/MonitoredEndpoint.ts
@@ -0,0 +1,13 @@
+export default interface MonitoredEndpoint {
+ Name: string;
+ IsStale: boolean;
+ EndpointInstanceIds: string[];
+ Metrics: { [key: string]: MonitoredValues };
+ DisconnectedCount: number;
+ ConnectedCount: number;
+}
+
+export interface MonitoredValues {
+ Average?: number;
+ Points: number[];
+}
diff --git a/frontend/src/resources/MonitoringEndpoint.ts b/frontend/src/resources/MonitoringEndpoint.ts
new file mode 100644
index 0000000000..d849e1f570
--- /dev/null
+++ b/frontend/src/resources/MonitoringEndpoint.ts
@@ -0,0 +1,118 @@
+export interface Endpoint {
+ name: string;
+ errorCount: number;
+ serviceControlId: string;
+ isScMonitoringDisconnected: boolean;
+ metrics: EndpointMetrics;
+ isStale: boolean;
+ endpointInstanceIds: string[];
+ disconnectedCount: number;
+ connectedCount: number;
+}
+
+export interface DigestValues {
+ latest?: number;
+ average?: number;
+}
+
+export interface EndpointDigest {
+ [index: string]: DigestValues | undefined;
+ queueLength?: DigestValues;
+ throughput?: DigestValues;
+ retries?: DigestValues;
+ processingTime?: DigestValues;
+ criticalTime?: DigestValues;
+}
+
+export interface EndpointValues {
+ points: number[];
+ average: number;
+}
+
+export interface EndpointValuesWithTime extends EndpointValues {
+ timeAxisValues: string[]; //dates
+}
+
+export interface EndpointMetrics {
+ [index: string]: EndpointValues;
+ queueLength: EndpointValues;
+ throughput: EndpointValues;
+ retries: EndpointValues;
+ processingTime: EndpointValuesWithTime;
+ criticalTime: EndpointValuesWithTime;
+}
+
+export interface EndpointInstance {
+ name: string;
+ id: string;
+ isStale: boolean;
+ metrics: EndpointMetrics;
+}
+
+export interface ExtendedEndpointInstance extends EndpointInstance {
+ isScMonitoringDisconnected: boolean;
+ serviceControlId: string;
+ errorCount: number;
+ isStale: boolean;
+}
+
+export interface MessageType {
+ id: string;
+ typeName: string;
+ assemblyName: string;
+ assemblyVersion: string;
+ culture: string;
+ publicKeyToken: string;
+ metrics: EndpointMetrics;
+}
+
+export interface MessageTypeDetails {
+ typeName: string;
+ assemblyName: string;
+ assemblyVersion: string;
+ culture?: string;
+ publicKeyToken?: string;
+}
+
+export interface ExtendedMessageType extends MessageType {
+ shortName: string;
+ messageTypeHierarchy?: MessageTypeDetails[];
+ containsTypeHierarchy?: boolean;
+ tooltipText: string;
+}
+
+export interface EndpointDetails {
+ instances: EndpointInstance[];
+ digest: { metrics: EndpointDigest };
+ metricDetails: {
+ metrics: EndpointMetrics;
+ };
+ messageTypes: MessageType[];
+}
+
+export interface ExtendedEndpointDetails extends EndpointDetails {
+ instances: ExtendedEndpointInstance[];
+ isScMonitoringDisconnected: boolean;
+ serviceControlId: string;
+ errorCount: number;
+ isStale: boolean;
+}
+
+export interface GroupedEndpoint {
+ groupName: string;
+ shortName: string;
+ endpoint: Endpoint;
+}
+
+export interface EndpointGroup {
+ group: string;
+ endpoints: GroupedEndpoint[];
+}
+
+export interface EndpointDetailsError {
+ error?: string;
+}
+
+export function isError(obj: EndpointDetails | EndpointDetailsError): obj is EndpointDetailsError {
+ return (obj as EndpointDetailsError).error !== undefined;
+}
diff --git a/frontend/src/resources/MonitoringResources.ts b/frontend/src/resources/MonitoringResources.ts
new file mode 100644
index 0000000000..4e8fb9d081
--- /dev/null
+++ b/frontend/src/resources/MonitoringResources.ts
@@ -0,0 +1,56 @@
+export interface MonitoringResource {
+ name: string;
+ label: string;
+ unit?: string;
+ tooltip?: string;
+}
+
+export const MessageType: MonitoringResource = {
+ name: "message-type-name",
+ label: "Message type name",
+};
+
+export const InstanceName: MonitoringResource = {
+ name: "instanceName",
+ label: "Instance Name",
+};
+
+export const EndpointName: MonitoringResource = {
+ name: "name",
+ label: "Endpoint Name",
+};
+
+export const Throughput: MonitoringResource = {
+ name: "throughput",
+ label: "Throughput",
+ unit: "(msgs/s)",
+ tooltip: "Throughput: The number of messages per second successfully processed by a receiving endpoint.",
+};
+
+export const ScheduledRetries: MonitoringResource = {
+ name: "retries",
+ label: "Scheduled retries",
+ unit: "(msgs/s)",
+ tooltip: "Scheduled retries: The number of messages per second scheduled for retries (immediate or delayed).",
+};
+
+export const ProcessingTime: MonitoringResource = {
+ name: "processingTime",
+ label: "Processing time",
+ unit: "(t)",
+ tooltip: "Processing time: The time taken for a receiving endpoint to successfully process a message.",
+};
+
+export const CriticalTime: MonitoringResource = {
+ name: "criticalTime",
+ label: "Critical time",
+ unit: "(t)",
+ tooltip: "Critical time: The elapsed time from when a message was sent, until it was successfully processed by a receiving endpoint.",
+};
+
+export const QueueLength: MonitoringResource = {
+ name: "queueLength",
+ label: "Queue length",
+ unit: "(msgs)",
+ tooltip: "Queue length: The number of messages waiting to be processed in the input queue(s) of the endpoint.",
+};
diff --git a/frontend/src/resources/QueueAddress.ts b/frontend/src/resources/QueueAddress.ts
new file mode 100644
index 0000000000..4079180b70
--- /dev/null
+++ b/frontend/src/resources/QueueAddress.ts
@@ -0,0 +1,4 @@
+export default interface QueueAddress {
+ physical_address: string;
+ failed_message_count: number;
+}
diff --git a/frontend/src/resources/RecoverabilityHistoryResponse.ts b/frontend/src/resources/RecoverabilityHistoryResponse.ts
new file mode 100644
index 0000000000..192129b33e
--- /dev/null
+++ b/frontend/src/resources/RecoverabilityHistoryResponse.ts
@@ -0,0 +1,8 @@
+import HistoricRetryOperation from "./HistoricRetryOperation";
+import UnacknowledgedRetryOperation from "./UnacknowledgedRetryOperation";
+
+export default interface RecoverabilityHistoryResponse {
+ id: string;
+ historic_operations: HistoricRetryOperation[];
+ unacknowledged_operations: UnacknowledgedRetryOperation[];
+}
diff --git a/frontend/src/resources/Redirect.ts b/frontend/src/resources/Redirect.ts
new file mode 100644
index 0000000000..8f139a94fa
--- /dev/null
+++ b/frontend/src/resources/Redirect.ts
@@ -0,0 +1,6 @@
+export default interface Redirect {
+ message_redirect_id: string;
+ from_physical_address: string;
+ to_physical_address: string;
+ last_modified: string;
+}
diff --git a/frontend/src/resources/Release.ts b/frontend/src/resources/Release.ts
new file mode 100644
index 0000000000..eab381230f
--- /dev/null
+++ b/frontend/src/resources/Release.ts
@@ -0,0 +1,5 @@
+export default interface Release {
+ tag: string;
+ release: string;
+ published: string;
+}
diff --git a/frontend/src/resources/ReportGenerationState.ts b/frontend/src/resources/ReportGenerationState.ts
new file mode 100644
index 0000000000..d7103b2555
--- /dev/null
+++ b/frontend/src/resources/ReportGenerationState.ts
@@ -0,0 +1,5 @@
+export default interface ReportGenerationState {
+ transport: string;
+ report_can_be_generated: boolean;
+ reason: string;
+}
diff --git a/frontend/src/resources/RetryType.ts b/frontend/src/resources/RetryType.ts
new file mode 100644
index 0000000000..130ce362cb
--- /dev/null
+++ b/frontend/src/resources/RetryType.ts
@@ -0,0 +1,9 @@
+export enum RetryType {
+ Unknown = "Unknown",
+ SingleMessage = "SingleMessage",
+ FailureGroup = "FailureGroup",
+ MultipleMessages = "MultipleMessages",
+ AllForEndpoint = "AllForEndpoint",
+ All = "All",
+ ByQueueAddress = "ByQueueAddress",
+}
diff --git a/frontend/src/resources/RootUrls.ts b/frontend/src/resources/RootUrls.ts
new file mode 100644
index 0000000000..5c18ca5c65
--- /dev/null
+++ b/frontend/src/resources/RootUrls.ts
@@ -0,0 +1,20 @@
+export default interface RootUrls {
+ description: string;
+ endpoints_error_url: string;
+ known_endpoints_url: string;
+ endpoints_message_search_url: string;
+ endpoints_messages_url: string;
+ audit_count_url: string;
+ endpoints_url: string;
+ errors_url: string;
+ configuration: string;
+ remote_configuration: string;
+ message_search_url: string;
+ license_status: string;
+ license_details: string;
+ name: string;
+ sagas_url: string;
+ event_log_items: string;
+ archived_groups_url: string;
+ get_archive_group: string;
+}
diff --git a/frontend/src/resources/SagaHistory.ts b/frontend/src/resources/SagaHistory.ts
new file mode 100644
index 0000000000..216b754d72
--- /dev/null
+++ b/frontend/src/resources/SagaHistory.ts
@@ -0,0 +1,36 @@
+import { MessageStatus } from "./Message";
+
+export interface SagaHistory {
+ id: string;
+ saga_id: string;
+ saga_type: string;
+ changes: SagaStateChange[];
+}
+
+export interface SagaStateChange {
+ start_time: Date;
+ finish_time: Date;
+ status: string;
+ state_after_change: string;
+ initiating_message: SagaMessage;
+ outgoing_messages: OutgoingMessage[];
+ endpoint: string;
+}
+
+export interface SagaMessage {
+ message_id: string;
+ is_saga_timeout_message: boolean;
+ originating_endpoint: string;
+ originating_machine: string;
+ time_sent: Date;
+ message_type: string;
+ intent: string;
+ body_url: string;
+ message_status: MessageStatus;
+}
+
+export interface OutgoingMessage extends SagaMessage {
+ delivery_delay?: string;
+ destination: string;
+ deliver_at: Date;
+}
diff --git a/frontend/src/resources/SequenceDiagram/Endpoint.ts b/frontend/src/resources/SequenceDiagram/Endpoint.ts
new file mode 100644
index 0000000000..7951991910
--- /dev/null
+++ b/frontend/src/resources/SequenceDiagram/Endpoint.ts
@@ -0,0 +1,128 @@
+import { NServiceBusHeaders } from "../Header";
+import Message from "../Message";
+import { Handler } from "./Handler";
+
+export interface Endpoint {
+ readonly name: string;
+ readonly hosts: EndpointHost[];
+ readonly hostId: string;
+ readonly handlers: Handler[];
+ readonly host: string;
+ readonly version: string;
+ uiRef?: Element;
+ addHandler(handler: Handler): void;
+}
+
+export interface EndpointHost {
+ readonly host: string;
+ readonly hostId: string;
+ readonly versions: string[];
+}
+
+export function createProcessingEndpoint(message: Message): Endpoint {
+ return new EndpointItem(
+ message.receiving_endpoint.name,
+ message.receiving_endpoint.host,
+ message.receiving_endpoint.host_id,
+ message.receiving_endpoint.name === message.sending_endpoint.name && message.receiving_endpoint.host === message.sending_endpoint.host ? message.headers.find((h) => h.key === NServiceBusHeaders.NServiceBusVersion)?.value : undefined
+ );
+}
+
+export function createSendingEndpoint(message: Message): Endpoint {
+ return new EndpointItem(message.sending_endpoint.name, message.sending_endpoint.host, message.sending_endpoint.host_id, message.headers.find((h) => h.key === NServiceBusHeaders.NServiceBusVersion)?.value);
+}
+
+export class EndpointRegistry {
+ #store = new Map();
+
+ register(item: Endpoint) {
+ let endpoint = this.#store.get(item.name);
+ if (!endpoint) {
+ endpoint = item as EndpointItem;
+ this.#store.set(endpoint.name, endpoint);
+ }
+
+ item.hosts.forEach((host) => endpoint.addHost(host as Host));
+ }
+
+ get(item: Endpoint) {
+ return this.#store.get(item.name)! as Endpoint;
+ }
+}
+
+class EndpointItem implements Endpoint {
+ private _hosts: Map;
+ private _name: string;
+ private _handlers: Handler[] = [];
+ uiRef?: SVGElement;
+
+ constructor(name: string, host: string, id: string, version?: string) {
+ const initialHost = new Host(host, id, version);
+ this._hosts = new Map([[initialHost.equatableKey, initialHost]]);
+ this._name = name;
+ }
+
+ get name() {
+ return this._name;
+ }
+ get hosts() {
+ return [...this._hosts].map(([, host]) => host);
+ }
+ get host() {
+ return [...this._hosts].map(([, host]) => host.host).join(",");
+ }
+ get hostId() {
+ return [...this._hosts].map(([, host]) => host.hostId).join(",");
+ }
+ get handlers() {
+ return [...this._handlers];
+ }
+ get version() {
+ return [...this._hosts].flatMap(([, host]) => host.versions).join(",");
+ }
+
+ addHost(host: Host) {
+ if (!this._hosts.has(host.equatableKey)) {
+ this._hosts.set(host.equatableKey, host);
+ } else {
+ const existing = this._hosts.get(host.equatableKey)!;
+ existing.addVersions(host.versions);
+ }
+ }
+
+ addHandler(handler: Handler) {
+ this._handlers.push(handler);
+ }
+}
+
+class Host implements EndpointHost {
+ private _host: string;
+ private _hostId: string;
+ private _versions: Set;
+
+ constructor(host: string, hostId: string, version?: string) {
+ this._host = host;
+ this._hostId = hostId;
+ this._versions = new Set();
+ this.addVersions([version]);
+ }
+
+ get host() {
+ return this._host;
+ }
+ get hostId() {
+ return this._hostId;
+ }
+
+ get versions() {
+ return [...this._versions];
+ }
+
+ get equatableKey() {
+ return `${this._hostId}###${this._host}`;
+ }
+
+ addVersions(versions: (string | undefined)[]) {
+ versions.filter((version) => version).forEach((version) => this._versions.add(version!.toLowerCase()));
+ }
+}
diff --git a/frontend/src/resources/SequenceDiagram/Handler.ts b/frontend/src/resources/SequenceDiagram/Handler.ts
new file mode 100644
index 0000000000..a7bd3a0686
--- /dev/null
+++ b/frontend/src/resources/SequenceDiagram/Handler.ts
@@ -0,0 +1,138 @@
+import { NServiceBusHeaders } from "../Header";
+import Message, { MessageStatus } from "../Message";
+import { Direction, MessageProcessingRoute, RoutedMessage } from "./RoutedMessage";
+import { Endpoint } from "./Endpoint";
+import { friendlyTypeName } from "./SequenceModel";
+
+export interface Handler {
+ readonly id: string;
+ name?: string;
+ friendlyName?: string;
+ readonly endpoint: Endpoint;
+ readonly isPartOfSaga: boolean;
+ partOfSaga?: string;
+ state: HandlerState;
+ inMessage?: RoutedMessage;
+ readonly outMessages: RoutedMessage[];
+ processedAt?: Date;
+ readonly handledAt?: Date;
+ processingTime?: number;
+ readonly direction: Direction;
+ route?: MessageProcessingRoute;
+ readonly selectedMessage?: Message;
+ uiRef?: SVGElement;
+ updateProcessedAt(timeSent: Date): void;
+ addOutMessage(routedMessage: RoutedMessage): void;
+}
+
+export enum HandlerState {
+ Fail,
+ Success,
+ Unknown,
+}
+
+export const ConversationStartHandlerName = "First";
+
+export function createSendingHandler(message: Message, sendingEndpoint: Endpoint): Handler {
+ return new HandlerItem(message.headers.find((h) => h.key === NServiceBusHeaders.RelatedTo)?.value ?? ConversationStartHandlerName, sendingEndpoint);
+}
+
+export function createProcessingHandler(message: Message, processingEndpoint: Endpoint): Handler {
+ const handler = new HandlerItem(message.message_id, processingEndpoint);
+ updateProcessingHandler(handler, message);
+ return handler;
+}
+
+export class HandlerRegistry {
+ #store = new Map();
+ private storeKey = (id: string, endpointName: string) => `${id}###${endpointName}`;
+
+ register(handler: Handler) {
+ const existing = this.#store.get(this.storeKey(handler.id, handler.endpoint.name));
+ if (existing) return { handler: existing, isNew: false };
+
+ this.#store.set(this.storeKey(handler.id, handler.endpoint.name), handler as HandlerItem);
+ return { handler, isNew: true };
+ }
+}
+
+export function updateProcessingHandler(handler: Handler, message: Message) {
+ handler.processedAt = new Date(message.processed_at);
+ //assuming if we have days in the timespan then something is very, very wrong
+ //TODO: extract logic since it's also currently used in AuditList
+ const [hh, mm, ss] = message.processing_time.split(":");
+ handler.processingTime = ((parseInt(hh) * 60 + parseInt(mm)) * 60 + parseFloat(ss)) * 1000;
+ handler.name = message.message_type;
+ handler.friendlyName = friendlyTypeName(message.message_type);
+
+ if ((message.invoked_sagas?.length ?? 0) > 0) {
+ handler.partOfSaga = message.invoked_sagas!.map((saga) => friendlyTypeName(saga.saga_type)).join(", ");
+ }
+
+ switch (message.status) {
+ case MessageStatus.ArchivedFailure:
+ case MessageStatus.Failed:
+ case MessageStatus.RepeatedFailure:
+ handler.state = HandlerState.Fail;
+ break;
+ default:
+ handler.state = HandlerState.Success;
+ }
+}
+
+class HandlerItem implements Handler {
+ private _id: string;
+ private _endpoint: Endpoint;
+ private _processedAtGuess?: Date;
+ private _outMessages: RoutedMessage[];
+ name?: string;
+ partOfSaga?: string;
+ inMessage?: RoutedMessage;
+ state: HandlerState = HandlerState.Unknown;
+ processedAt?: Date;
+ processingTime?: number;
+ route?: MessageProcessingRoute;
+ uiRef?: SVGElement;
+
+ constructor(id: string, endpoint: Endpoint) {
+ this._id = id;
+ this._endpoint = endpoint;
+ this._outMessages = [];
+ }
+
+ get id() {
+ return this._id;
+ }
+
+ get endpoint() {
+ return this._endpoint;
+ }
+
+ get isPartOfSaga() {
+ return this.partOfSaga != null;
+ }
+
+ get handledAt() {
+ return this.processedAt ?? this._processedAtGuess;
+ }
+
+ get selectedMessage() {
+ return this.route?.fromRoutedMessage?.selectedMessage;
+ }
+
+ get outMessages() {
+ return [...this._outMessages];
+ }
+
+ get direction() {
+ return this.outMessages[0]?.direction ?? Direction.Right;
+ }
+
+ updateProcessedAt(timeSent: Date) {
+ if (!this._processedAtGuess || this._processedAtGuess.getTime() > timeSent.getTime()) this._processedAtGuess = timeSent;
+ }
+
+ addOutMessage(routedMessage: RoutedMessage) {
+ this._outMessages = [routedMessage, ...this._outMessages].sort((a, b) => (a.sentTime?.getTime() ?? 0) - (b.sentTime?.getTime() ?? 0));
+ }
+}
diff --git a/frontend/src/resources/SequenceDiagram/RoutedMessage.ts b/frontend/src/resources/SequenceDiagram/RoutedMessage.ts
new file mode 100644
index 0000000000..c4c3c85415
--- /dev/null
+++ b/frontend/src/resources/SequenceDiagram/RoutedMessage.ts
@@ -0,0 +1,117 @@
+import EndpointDetails from "../EndpointDetails";
+import { NServiceBusHeaders } from "../Header";
+import Message, { MessageIntent, MessageStatus } from "../Message";
+import { Handler } from "./Handler";
+import { friendlyTypeName } from "./SequenceModel";
+
+export interface RoutedMessage {
+ id: string;
+ name: string;
+ readonly selectedMessage: Message;
+ fromHandler?: Handler;
+ toHandler?: Handler;
+ route?: MessageProcessingRoute;
+ direction: Direction;
+ type: RoutedMessageType;
+ readonly receiving: EndpointDetails;
+ readonly sending: EndpointDetails;
+ readonly sentTime: Date | undefined;
+ readonly messageId: string;
+ readonly status: MessageStatus;
+}
+
+export interface MessageProcessingRoute {
+ readonly name?: string;
+ readonly fromRoutedMessage?: RoutedMessage;
+ readonly processingHandler?: Handler;
+ uiRef?: SVGElement;
+}
+
+export enum Direction {
+ Left,
+ Right,
+}
+
+export enum RoutedMessageType {
+ Event,
+ Command,
+ Local,
+ Timeout,
+}
+
+export function createRoute(routedMessage: RoutedMessage, processingHandler: Handler): MessageProcessingRoute {
+ return new MessageProcessingRouteItem(routedMessage, processingHandler);
+}
+
+export function createRoutedMessage(message: Message): RoutedMessage {
+ const routedMessage = new RoutedMessageItem(message);
+
+ if (message.message_intent === MessageIntent.Publish) routedMessage.type = RoutedMessageType.Event;
+ else {
+ const isTimeoutString = message.headers.find((h) => h.key === NServiceBusHeaders.IsSagaTimeoutMessage)?.value?.toLowerCase();
+ const isTimeout = (isTimeoutString ?? "") === "true";
+ if (isTimeout) routedMessage.type = RoutedMessageType.Timeout;
+ else if (message.receiving_endpoint.host_id === message.sending_endpoint.host_id && message.receiving_endpoint.name === message.sending_endpoint.name) routedMessage.type = RoutedMessageType.Local;
+ else routedMessage.type = RoutedMessageType.Command;
+ }
+
+ return routedMessage;
+}
+
+class MessageProcessingRouteItem implements MessageProcessingRoute {
+ readonly name?: string;
+ private _fromRoutedMessage?: RoutedMessageItem;
+ readonly processingHandler?: Handler;
+ uiRef?: SVGElement;
+
+ constructor(routedMessage?: RoutedMessageItem, processingHandler?: Handler) {
+ this._fromRoutedMessage = routedMessage;
+ this.processingHandler = processingHandler;
+
+ if (routedMessage && this.processingHandler) {
+ this.name = `${processingHandler?.name}(${routedMessage.id})`;
+ }
+
+ if (routedMessage) routedMessage.route = this;
+ if (processingHandler) processingHandler.route = this;
+ }
+
+ get fromRoutedMessage() {
+ return this._fromRoutedMessage as RoutedMessage | undefined;
+ }
+}
+
+class RoutedMessageItem implements RoutedMessage {
+ readonly selectedMessage: Message;
+ readonly name: string;
+ fromHandler?: Handler;
+ toHandler?: Handler;
+ route?: MessageProcessingRoute;
+ direction = Direction.Left;
+ type = RoutedMessageType.Command;
+
+ constructor(message: Message) {
+ this.selectedMessage = message;
+ this.name = friendlyTypeName(message.message_type) ?? "";
+ }
+
+ get id() {
+ return this.selectedMessage.id;
+ }
+
+ get receiving() {
+ return this.selectedMessage.receiving_endpoint;
+ }
+ get sending() {
+ return this.selectedMessage.sending_endpoint;
+ }
+ get sentTime() {
+ return this.selectedMessage.time_sent ? new Date(this.selectedMessage.time_sent) : undefined;
+ }
+ get messageId() {
+ return this.selectedMessage.message_id;
+ }
+ get status() {
+ return this.selectedMessage.status;
+ }
+}
diff --git a/frontend/src/resources/SequenceDiagram/SequenceModel.ts b/frontend/src/resources/SequenceDiagram/SequenceModel.ts
new file mode 100644
index 0000000000..3ce04dad66
--- /dev/null
+++ b/frontend/src/resources/SequenceDiagram/SequenceModel.ts
@@ -0,0 +1,142 @@
+import { NServiceBusHeaders } from "../Header";
+import Message from "../Message";
+import { createRoutedMessage, createRoute, MessageProcessingRoute } from "./RoutedMessage";
+import { createProcessingEndpoint, createSendingEndpoint, Endpoint, EndpointRegistry } from "./Endpoint";
+import { ConversationStartHandlerName, createProcessingHandler, createSendingHandler, Handler, HandlerRegistry, updateProcessingHandler } from "./Handler";
+
+export interface ConversationModel {
+ endpoints: Endpoint[];
+}
+
+//TODO: extract to common area if this continues to be used in AuditList
+export function friendlyTypeName(messageType: string) {
+ if (messageType == null) return undefined;
+
+ const typeClass = messageType.split(",")[0];
+ const typeName = typeClass.split(".").reverse()[0];
+ return typeName.replace(/\+/g, ".");
+}
+
+export class ModelCreator implements ConversationModel {
+ #endpoints: Endpoint[];
+ #handlers: Handler[];
+ #processingRoutes: MessageProcessingRoute[];
+
+ constructor(messages: Message[]) {
+ this.#endpoints = [];
+ this.#processingRoutes = [];
+
+ const endpointRegistry = new EndpointRegistry();
+ const handlerRegistry = new HandlerRegistry();
+ const firstOrderHandlers: Handler[] = [];
+ const messagesInOrder = MessageTreeNode.createTree(messages).flatMap((node) => node.walk());
+
+ // NOTE: All sending endpoints are created first to ensure version info is retained
+ for (const message of messagesInOrder) {
+ endpointRegistry.register(createSendingEndpoint(message));
+ }
+ for (const message of messagesInOrder) {
+ endpointRegistry.register(createProcessingEndpoint(message));
+ }
+
+ for (const message of messagesInOrder) {
+ const sendingEndpoint = endpointRegistry.get(createSendingEndpoint(message));
+ if (!this.#endpoints.find((endpoint) => endpoint.name === sendingEndpoint?.name)) {
+ this.#endpoints.push(sendingEndpoint);
+ }
+ const processingEndpoint = endpointRegistry.get(createProcessingEndpoint(message));
+ if (!this.#endpoints.find((endpoint) => endpoint.name === processingEndpoint?.name)) {
+ this.#endpoints.push(processingEndpoint);
+ }
+
+ const { handler: sendingHandler, isNew: sendingHandlerIsNew } = handlerRegistry.register(createSendingHandler(message, sendingEndpoint));
+ if (sendingHandlerIsNew) {
+ firstOrderHandlers.push(sendingHandler);
+ sendingEndpoint.addHandler(sendingHandler);
+ }
+ sendingHandler.updateProcessedAt(new Date(message.time_sent));
+
+ const { handler: processingHandler, isNew: processingHandlerIsNew } = handlerRegistry.register(createProcessingHandler(message, processingEndpoint));
+ if (processingHandlerIsNew) {
+ firstOrderHandlers.push(processingHandler);
+ processingEndpoint.addHandler(processingHandler);
+ } else {
+ updateProcessingHandler(processingHandler, message);
+ }
+
+ const routedMessage = createRoutedMessage(message);
+ routedMessage.toHandler = processingHandler;
+ routedMessage.fromHandler = sendingHandler;
+ this.#processingRoutes.push(createRoute(routedMessage, processingHandler));
+ processingHandler.inMessage = routedMessage;
+ sendingHandler.addOutMessage(routedMessage);
+ }
+
+ const start = firstOrderHandlers.filter((h) => h.id === ConversationStartHandlerName);
+ const orderByHandledAt = firstOrderHandlers.filter((h) => h.id !== ConversationStartHandlerName).sort((a, b) => (a.handledAt?.getTime() ?? 0) - (b.handledAt?.getTime() ?? 0));
+
+ this.#handlers = [...start, ...orderByHandledAt];
+ }
+
+ get endpoints(): Endpoint[] {
+ return [...this.#endpoints];
+ }
+
+ get handlers(): Handler[] {
+ return [...this.#handlers];
+ }
+
+ get routes(): MessageProcessingRoute[] {
+ return [...this.#processingRoutes];
+ }
+}
+
+class MessageTreeNode {
+ #message: Message;
+ #parent?: string;
+ #children: MessageTreeNode[];
+
+ static createTree(messages: Message[]) {
+ const nodes = messages.map((message) => new MessageTreeNode(message));
+ const resolved: MessageTreeNode[] = [];
+ const index = new Map(nodes.map((node) => [node.id, node]));
+
+ for (const node of nodes) {
+ const parent = index.get(node.parent ?? "");
+ if (parent) {
+ parent.addChild(node);
+ resolved.push(node);
+ }
+ }
+
+ return nodes.filter((node) => !resolved.includes(node));
+ }
+
+ constructor(message: Message) {
+ this.#message = message;
+ this.#parent = message.headers.find((h) => h.key === NServiceBusHeaders.RelatedTo)?.value;
+ this.#children = [];
+ }
+
+ get id() {
+ return this.#message.message_id;
+ }
+ get parent() {
+ return this.#parent;
+ }
+ get message() {
+ return this.#message;
+ }
+ get children() {
+ return [...this.#children];
+ }
+
+ addChild(childNode: MessageTreeNode) {
+ this.#children.push(childNode);
+ }
+
+ walk(): Message[] {
+ //TODO: check performance of this. We may need to pre-calculate the processed_at as a date on the message object
+ return [this.#message, ...this.children.sort((a, b) => new Date(a.message.processed_at).getTime() - new Date(b.message.processed_at).getTime()).flatMap((child) => child.walk())];
+ }
+}
diff --git a/frontend/src/resources/ServiceControlMonitoringInstance.ts b/frontend/src/resources/ServiceControlMonitoringInstance.ts
new file mode 100644
index 0000000000..21630a4fbf
--- /dev/null
+++ b/frontend/src/resources/ServiceControlMonitoringInstance.ts
@@ -0,0 +1,4 @@
+export interface ServiceControlMonitoringInstance {
+ instanceType: string;
+ version: string;
+}
diff --git a/frontend/src/resources/SortOptions.ts b/frontend/src/resources/SortOptions.ts
new file mode 100644
index 0000000000..50c58af235
--- /dev/null
+++ b/frontend/src/resources/SortOptions.ts
@@ -0,0 +1,19 @@
+import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import type { Moment } from "moment";
+
+export type GroupPropertyType = string | number | Date | Moment | boolean;
+
+export default interface SortOptions {
+ description: string;
+ iconAsc: IconDefinition;
+ iconDesc: IconDefinition;
+ dir?: SortDirection;
+ //used for client-side sorting only
+ selector?: (group: T) => GroupPropertyType;
+ sort?: (firstElement: T, secondElement: T) => number;
+}
+
+export enum SortDirection {
+ Ascending = "asc",
+ Descending = "desc",
+}
diff --git a/frontend/src/resources/ThroughputConnectionSettings.ts b/frontend/src/resources/ThroughputConnectionSettings.ts
new file mode 100644
index 0000000000..924d98cfd4
--- /dev/null
+++ b/frontend/src/resources/ThroughputConnectionSettings.ts
@@ -0,0 +1,10 @@
+export default interface ThroughputConnectionSettings {
+ service_control_settings: ThroughputConnectionSetting[];
+ monitoring_settings: ThroughputConnectionSetting[];
+ broker_settings: ThroughputConnectionSetting[];
+}
+
+export interface ThroughputConnectionSetting {
+ name: string;
+ description: string;
+}
diff --git a/frontend/src/resources/UnacknowledgedRetryOperation.ts b/frontend/src/resources/UnacknowledgedRetryOperation.ts
new file mode 100644
index 0000000000..d8ec4fbf23
--- /dev/null
+++ b/frontend/src/resources/UnacknowledgedRetryOperation.ts
@@ -0,0 +1,13 @@
+import { RetryType } from "./RetryType";
+
+export default interface UnacknowledgedRetryOperation {
+ request_id: string;
+ retry_type: RetryType;
+ start_time: string;
+ completion_time: string;
+ last: string;
+ originator: string;
+ classifier: string;
+ failed: boolean;
+ number_of_messages_processed: number;
+}
diff --git a/frontend/src/resources/UpdateEmailNotificationsSettingsRequest.ts b/frontend/src/resources/UpdateEmailNotificationsSettingsRequest.ts
new file mode 100644
index 0000000000..f88207ba79
--- /dev/null
+++ b/frontend/src/resources/UpdateEmailNotificationsSettingsRequest.ts
@@ -0,0 +1,9 @@
+export default interface UpdateEmailNotificationsSettingsRequest {
+ smtp_server: string;
+ smtp_port: number;
+ authorization_account: string;
+ authorization_password: string;
+ enable_tls: boolean;
+ to: string;
+ from: string;
+}
diff --git a/frontend/src/resources/UpdateUserIndicator.ts b/frontend/src/resources/UpdateUserIndicator.ts
new file mode 100644
index 0000000000..10acfdf6a6
--- /dev/null
+++ b/frontend/src/resources/UpdateUserIndicator.ts
@@ -0,0 +1,6 @@
+interface UpdateUserIndicator {
+ name: string;
+ user_indicator: string;
+}
+
+export default UpdateUserIndicator;
diff --git a/frontend/src/router/config.ts b/frontend/src/router/config.ts
new file mode 100644
index 0000000000..63da573eff
--- /dev/null
+++ b/frontend/src/router/config.ts
@@ -0,0 +1,228 @@
+import DashboardView from "@/views/DashboardView.vue";
+import type { RouteComponent } from "vue-router";
+import FailedMessagesView from "@/views/FailedMessagesView.vue";
+import MonitoringView from "@/views/MonitoringView.vue";
+import EventsView from "@/views/EventsView.vue";
+import ConfigurationView from "@/views/ConfigurationView.vue";
+import routeLinks from "@/router/routeLinks";
+import CustomChecksView from "@/views/CustomChecksView.vue";
+import HeartbeatsView from "@/views/HeartbeatsView.vue";
+import ThroughputReportView from "@/views/ThroughputReportView.vue";
+import AuditView from "@/views/AuditView.vue";
+
+export interface RouteItem {
+ path: string;
+ alias?: string;
+ redirect?: string;
+ title: string;
+ component?: RouteComponent | (() => Promise);
+ children?: RouteItem[];
+}
+
+const config: RouteItem[] = [
+ {
+ path: routeLinks.dashboard,
+ component: DashboardView,
+ title: "Dashboard",
+ },
+ {
+ path: routeLinks.heartbeats.instances.template,
+ component: () => import("@/components/heartbeats/EndpointInstances.vue"),
+ title: "Endpoint Instances",
+ },
+ {
+ path: routeLinks.heartbeats.root,
+ component: HeartbeatsView,
+ title: "Heartbeats",
+ redirect: routeLinks.heartbeats.unhealthy.link,
+ children: [
+ {
+ title: "Unhealthy Endpoints",
+ path: routeLinks.heartbeats.unhealthy.link,
+ component: () => import("@/components/heartbeats/UnhealthyEndpoints.vue"),
+ },
+ {
+ title: "Healthy Endpoints",
+ path: routeLinks.heartbeats.healthy.link,
+ component: () => import("@/components/heartbeats/HealthyEndpoints.vue"),
+ },
+ {
+ title: "Heartbeat Configuration",
+ path: routeLinks.heartbeats.configuration.link,
+ component: () => import("@/components/heartbeats/HeartbeatConfiguration.vue"),
+ },
+ ],
+ },
+ {
+ path: routeLinks.messages.root,
+ component: AuditView,
+ title: "All Messages",
+ },
+ {
+ path: routeLinks.failedMessage.root,
+ component: FailedMessagesView,
+ title: "Failed Messages",
+ redirect: routeLinks.failedMessage.failedMessagesGroups.link,
+ children: [
+ {
+ title: "Failed Message Groups",
+ path: routeLinks.failedMessage.failedMessagesGroups.template,
+ component: () => import("@/components/failedmessages/FailedMessageGroups.vue"),
+ },
+ {
+ path: routeLinks.failedMessage.failedMessages.template,
+ title: "All Failed Messages",
+ component: () => import("@/components/failedmessages/FailedMessages.vue"),
+ },
+ {
+ path: routeLinks.failedMessage.deletedMessagesGroup.template,
+ title: "Deleted Message Groups",
+ component: () => import("@/components/failedmessages/DeletedMessageGroups.vue"),
+ },
+ {
+ path: routeLinks.failedMessage.deletedMessages.template,
+ title: "All Deleted Messages",
+ component: () => import("@/components/failedmessages/DeletedMessages.vue"),
+ },
+ {
+ path: routeLinks.failedMessage.pendingRetries.template,
+ title: "Pending Retries",
+ component: () => import("@/components/failedmessages/PendingRetries.vue"),
+ },
+ {
+ title: "Failed Messages",
+ path: routeLinks.failedMessage.group.template,
+ component: () => import("@/components/failedmessages/FailedMessages.vue"),
+ },
+ {
+ title: "Deleted Messages",
+ path: routeLinks.failedMessage.deletedGroup.template,
+ component: () => import("@/components/failedmessages/DeletedMessages.vue"),
+ },
+ {
+ path: routeLinks.failedMessage.message.template,
+ title: "Message",
+ redirect: routeLinks.messages.failedMessage.template,
+ },
+ ],
+ },
+ {
+ path: routeLinks.messages.failedMessage.template,
+ title: "Message",
+ component: () => import("@/components/messages/MessageView.vue"),
+ },
+ {
+ path: routeLinks.messages.successMessage.template,
+ title: "Message",
+ component: () => import("@/components/messages/MessageView.vue"),
+ },
+ {
+ path: routeLinks.monitoring.root,
+ component: MonitoringView,
+ title: "Monitored Endpoints",
+ },
+ {
+ path: routeLinks.monitoring.endpointDetails.template,
+ component: () => import("@/components/monitoring/EndpointDetails.vue"),
+ title: "Endpoint Details",
+ },
+ {
+ path: routeLinks.customChecks,
+ title: "Custom checks",
+ component: CustomChecksView,
+ },
+ {
+ path: routeLinks.events,
+ component: EventsView,
+ title: "Events",
+ },
+ {
+ path: routeLinks.throughput.root,
+ component: ThroughputReportView,
+ title: "Usage",
+ redirect: routeLinks.throughput.endpoints.root,
+ children: [
+ {
+ title: "Endpoints",
+ path: routeLinks.throughput.endpoints.root,
+ redirect: routeLinks.throughput.endpoints.detectedEndpoints.link,
+ component: () => import("@/views/throughputreport/EndpointsView.vue"),
+ children: [
+ {
+ title: "Detected Endpoints",
+ path: routeLinks.throughput.endpoints.detectedEndpoints.template,
+ component: () => import("@/views/throughputreport/endpoints/DetectedEndpointsView.vue"),
+ },
+ {
+ title: "Detected Broker Queues",
+ path: routeLinks.throughput.endpoints.detectedBrokerQueues.template,
+ component: () => import("@/views/throughputreport/endpoints/DetectedBrokerQueuesView.vue"),
+ },
+ ],
+ },
+ ],
+ },
+ {
+ path: routeLinks.configuration.root,
+ title: "Configuration",
+ component: ConfigurationView,
+ redirect: routeLinks.configuration.license.link,
+ children: [
+ {
+ title: "License",
+ path: routeLinks.configuration.license.template,
+ component: () => import("@/components/configuration/PlatformLicense.vue"),
+ },
+ {
+ title: "MassTransit Connector",
+ path: routeLinks.configuration.massTransitConnector.template,
+ component: () => import("@/components/configuration/MassTransitConnector.vue"),
+ },
+ {
+ title: "Health Check Notifications",
+ path: routeLinks.configuration.healthCheckNotifications.template,
+ component: () => import("@/components/configuration/HealthCheckNotifications.vue"),
+ },
+ {
+ title: "Retry Redirects",
+ path: routeLinks.configuration.retryRedirects.template,
+ component: () => import("@/components/configuration/RetryRedirects.vue"),
+ },
+ {
+ title: "Connections",
+ path: routeLinks.configuration.connections.template,
+ component: () => import("@/components/configuration/PlatformConnections.vue"),
+ },
+ {
+ title: "Endpoint Connection",
+ path: routeLinks.configuration.endpointConnection.template,
+ component: () => import("@/components/configuration/EndpointConnection.vue"),
+ },
+ {
+ title: "Usage Setup",
+ path: routeLinks.throughput.setup.root,
+ redirect: routeLinks.throughput.setup.connectionSetup.link,
+ component: () => import("@/views/throughputreport/SetupView.vue"),
+ children: [
+ {
+ title: "Connection Setup",
+ path: routeLinks.throughput.setup.connectionSetup.template,
+ component: () => import("@/views/throughputreport/setup/ConnectionSetupView.vue"),
+ },
+ {
+ title: "Mask Report Data",
+ path: routeLinks.throughput.setup.mask.template,
+ component: () => import("@/views/throughputreport/setup/MasksView.vue"),
+ },
+ {
+ title: "Diagnostics",
+ path: routeLinks.throughput.setup.diagnostics.template,
+ component: () => import("@/views/throughputreport/setup/DiagnosticsView.vue"),
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export default config;
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
new file mode 100644
index 0000000000..66ddf21af6
--- /dev/null
+++ b/frontend/src/router/index.ts
@@ -0,0 +1,60 @@
+import { createRouter, createWebHashHistory, type RouteRecordRaw, RouteRecordSingleViewWithChildren } from "vue-router";
+import config, { RouteItem } from "./config";
+import { getDefaultConfig } from "@/defaultConfig";
+
+function meta(item: { title: string }) {
+ return { title: `${item.title} • ServicePulse` };
+}
+
+function addChildren(parent: RouteRecordSingleViewWithChildren, item: RouteItem) {
+ if (item.children) {
+ item.children.forEach((child) => {
+ const newItem: RouteRecordSingleViewWithChildren = {
+ path: child.path,
+ name: `${item.path}/${child.path}`,
+ meta: meta(child),
+ component: child.component,
+ children: [],
+ };
+ parent.children.push(newItem);
+
+ if (child.redirect) newItem.redirect = child.redirect;
+ if (child.alias) newItem.alias = child.alias;
+
+ addChildren(newItem, child);
+ });
+ }
+}
+
+export default function makeRouter() {
+ const routes = config.map((item) => {
+ const result: RouteRecordSingleViewWithChildren = {
+ path: item.path,
+ name: item.path,
+ meta: meta(item),
+ component: item.component,
+ children: [],
+ };
+
+ addChildren(result, item);
+
+ if (item.redirect) result.redirect = item.redirect;
+ if (item.alias) result.alias = item.alias;
+
+ return result;
+ });
+
+ const defaultRoute = getDefaultConfig().default_route;
+ if (!!defaultRoute && defaultRoute !== "/") {
+ routes.push({
+ path: "/",
+ redirect: defaultRoute,
+ });
+ }
+
+ return createRouter({
+ history: createWebHashHistory(),
+ routes: routes,
+ strict: false,
+ });
+}
diff --git a/frontend/src/router/routeLinks.ts b/frontend/src/router/routeLinks.ts
new file mode 100644
index 0000000000..9cb63ed9a6
--- /dev/null
+++ b/frontend/src/router/routeLinks.ts
@@ -0,0 +1,112 @@
+const heartbeatLinks = (root: string) => {
+ function createLink(template: string) {
+ return { link: `${root}/${template}`, template: template };
+ }
+
+ return {
+ root,
+ unhealthy: createLink("unhealthy"),
+ healthy: createLink("healthy"),
+ configuration: createLink("configuration"),
+ instances: { link: (endpointName: string) => `${root}/instances/${encodeURIComponent(endpointName)}`, template: "/heartbeats/instances/:endpointName" },
+ };
+};
+
+const failedMessagesLinks = (root: string) => {
+ function createLink(template: string) {
+ return { link: `${root}/${template}`, template: template };
+ }
+
+ return {
+ root,
+ failedMessagesGroups: createLink("failed-message-groups"),
+ failedMessages: createLink("all-failed-messages"),
+ deletedMessagesGroup: createLink("deleted-message-groups"),
+ deletedMessages: createLink("all-deleted-messages"),
+ pendingRetries: createLink("pending-retries"),
+ group: { link: (groupId: string) => `${root}/group/${groupId}`, template: "group/:groupId" },
+ deletedGroup: { link: (groupId: string) => `${root}/deleted-messages/group/${groupId}`, template: "deleted-messages/group/:groupId" },
+ message: { link: (id: string) => `${root}/message/${id}`, template: "message/:id" },
+ };
+};
+
+const messagesLinks = (root: string) => {
+ return {
+ root,
+ failedMessage: { link: (id: string) => `${root}/${id}`, template: "/messages/:id" },
+ successMessage: { link: (messageId: string, id: string) => `${root}/${messageId}/${id}`, template: "/messages/:messageId/:id" },
+ };
+};
+
+const configurationLinks = (root: string) => {
+ function createLink(template: string) {
+ return { link: `${root}/${template}`, template: template };
+ }
+
+ return {
+ root,
+ license: createLink("license"),
+ massTransitConnector: createLink("mass-transit-connector"),
+ healthCheckNotifications: createLink("health-check-notifications"),
+ retryRedirects: createLink("retry-redirects"),
+ connections: createLink("connections"),
+ endpointConnection: createLink("endpoint-connection"),
+ };
+};
+
+const throughputLinks = (root: string) => {
+ return {
+ root: root,
+ endpoints: throughputEndpointLinks(`${root}/endpoints`),
+ setup: throughputSetupLinks(`${root}/setup`),
+ };
+};
+
+const throughputSetupLinks = (root: string) => {
+ function createLink(template: string) {
+ return { link: `${root}/${template}`, template: template };
+ }
+
+ return {
+ root,
+ connectionSetup: createLink("connection-setup"),
+ mask: createLink("mask"),
+ diagnostics: createLink("diagnostics"),
+ };
+};
+
+const throughputEndpointLinks = (root: string) => {
+ function createLink(template: string) {
+ return { link: `${root}/${template}`, template: template };
+ }
+
+ return {
+ root,
+ detectedEndpoints: createLink("known"),
+ detectedBrokerQueues: createLink("broker"),
+ };
+};
+
+const monitoringLinks = (root: string) => {
+ return {
+ root,
+ endpointDetails: {
+ link: (endpointName: string, historyPeriod: number, tab?: string) => `${root}/endpoint/${encodeURIComponent(endpointName)}?historyPeriod=${historyPeriod}${(tab && `&tab=${tab}`) ?? ""}`,
+ template: "/monitoring/endpoint/:endpointName",
+ },
+ };
+};
+
+const routeLinks = {
+ dashboard: "/dashboard",
+ heartbeats: heartbeatLinks("/heartbeats"),
+ monitoring: monitoringLinks("/monitoring"),
+ failedMessage: failedMessagesLinks("/failed-messages"),
+ customChecks: "/custom-checks",
+ events: "/events",
+ messages: messagesLinks("/messages"),
+ configuration: configurationLinks("/configuration"),
+ throughput: throughputLinks("/usage"),
+};
+
+export default routeLinks;
diff --git a/frontend/src/router/vue-router.d.ts b/frontend/src/router/vue-router.d.ts
new file mode 100644
index 0000000000..c5173bdfba
--- /dev/null
+++ b/frontend/src/router/vue-router.d.ts
@@ -0,0 +1,13 @@
+// This can be directly added to any of your `.ts` files like `router.ts`
+// It can also be added to a `.d.ts` file. Make sure it's included in
+// project's tsconfig.json "files"
+import "vue-router";
+
+// To ensure it is treated as a module, add at least one `export` statement
+export {};
+
+declare module "vue-router" {
+ interface RouteMeta {
+ title: string;
+ }
+}
diff --git a/frontend/src/stores/AuditStore.ts b/frontend/src/stores/AuditStore.ts
new file mode 100644
index 0000000000..4e558c4ce4
--- /dev/null
+++ b/frontend/src/stores/AuditStore.ts
@@ -0,0 +1,76 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { ref } from "vue";
+import type { SortInfo } from "@/components/SortInfo";
+import Message from "@/resources/Message";
+import { EndpointsView } from "@/resources/EndpointView";
+import type { DateRange } from "@/types/date";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export enum FieldNames {
+ TimeSent = "time_sent",
+ ProcessingTime = "processing_time",
+ CriticalTime = "critical_time",
+ DeliveryTime = "delivery_time",
+}
+
+export const useAuditStore = defineStore("AuditStore", () => {
+ const serviceControlStore = useServiceControlStore();
+
+ const sortByInstances = ref({
+ property: FieldNames.TimeSent,
+ isAscending: false,
+ });
+
+ const dateRange = ref([]);
+ const messageFilterString = ref("");
+ const itemsPerPage = ref(100);
+ const totalCount = ref(0);
+ const messages = ref([]);
+ const selectedEndpointName = ref("");
+ const endpoints = ref([]);
+
+ async function loadEndpoints() {
+ try {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl(`endpoints`);
+ endpoints.value = data;
+ } catch (e) {
+ endpoints.value = [];
+ throw e;
+ }
+ }
+
+ async function refresh() {
+ try {
+ const [fromDate, toDate] = dateRange.value;
+ const from = fromDate?.toISOString() ?? "";
+ const to = toDate?.toISOString() ?? "";
+ const [response, data] = await serviceControlStore.fetchTypedFromServiceControl(
+ `messages2/?endpoint_name=${selectedEndpointName.value}&from=${from}&to=${to}&q=${messageFilterString.value}&page_size=${itemsPerPage.value}&sort=${sortByInstances.value.property}&direction=${sortByInstances.value.isAscending ? "asc" : "desc"}`
+ );
+ totalCount.value = parseInt(response.headers.get("total-count") ?? "0");
+ messages.value = data;
+ } catch (e) {
+ messages.value = [];
+ throw e;
+ }
+ }
+
+ return {
+ refresh,
+ loadEndpoints,
+ sortBy: sortByInstances,
+ messages,
+ messageFilterString,
+ selectedEndpointName,
+ itemsPerPage,
+ totalCount,
+ endpoints,
+ dateRange,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useAuditStore, import.meta.hot));
+}
+
+export type AuditStore = ReturnType;
diff --git a/frontend/src/stores/ConfigurationStore.ts b/frontend/src/stores/ConfigurationStore.ts
new file mode 100644
index 0000000000..b7e4b9fbd8
--- /dev/null
+++ b/frontend/src/stores/ConfigurationStore.ts
@@ -0,0 +1,34 @@
+import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia";
+import { computed, ref, watch } from "vue";
+import Configuration from "@/resources/Configuration";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export const useConfigurationStore = defineStore("ConfigurationStore", () => {
+ const configuration = ref(null);
+
+ const serviceControlStore = useServiceControlStore();
+ const { serviceControlUrl } = storeToRefs(serviceControlStore);
+
+ const isMassTransitConnected = computed(() => configuration.value?.mass_transit_connector !== undefined);
+
+ async function refresh() {
+ if (!serviceControlUrl.value) return;
+
+ const response = await serviceControlStore.fetchFromServiceControl("configuration");
+ configuration.value = await response.json();
+ }
+
+ watch(serviceControlUrl, refresh, { immediate: true });
+
+ return {
+ configuration,
+ refresh,
+ isMassTransitConnected,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useConfigurationStore, import.meta.hot));
+}
+
+export type ConfigurationStore = ReturnType;
diff --git a/frontend/src/stores/ConnectionsAndStatsStore.ts b/frontend/src/stores/ConnectionsAndStatsStore.ts
new file mode 100644
index 0000000000..42b7cf7c66
--- /dev/null
+++ b/frontend/src/stores/ConnectionsAndStatsStore.ts
@@ -0,0 +1,138 @@
+import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia";
+import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
+import { FailedMessage, FailedMessageStatus } from "@/resources/FailedMessage";
+import { ConnectionState } from "@/resources/ConnectionState";
+import { useCounter } from "@vueuse/core";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export const useConnectionsAndStatsStore = defineStore("ConnectionsAndStatsStore", () => {
+ const serviceControlStore = useServiceControlStore();
+ const { isMonitoringEnabled } = storeToRefs(serviceControlStore);
+
+ const failedMessageCount = ref(0);
+ const archivedMessageCount = ref(0);
+ const pendingRetriesMessageCount = ref(0);
+ const disconnectedEndpointsCount = ref(0);
+
+ const { count: requiresFullFailureDetailsSubscriberCount, inc, dec } = useCounter(0);
+ function requiresFullFailureDetails() {
+ onMounted(() => inc());
+ onUnmounted(() => dec());
+ }
+
+ const connectionState = reactive({
+ connected: false,
+ connecting: false,
+ connectedRecently: false,
+ unableToConnect: null,
+ });
+
+ const monitoringConnectionState = reactive({
+ connected: false,
+ connecting: false,
+ connectedRecently: false,
+ unableToConnect: null,
+ });
+
+ const displayConnectionsWarning = computed(() => (connectionState.unableToConnect || (monitoringConnectionState.unableToConnect && isMonitoringEnabled.value)) ?? false);
+
+ async function refresh() {
+ const failedMessagesResult = getErrorMessagesCount(FailedMessageStatus.Unresolved);
+ const archivedMessagesResult = requiresFullFailureDetailsSubscriberCount.value > 0 ? getErrorMessagesCount(FailedMessageStatus.Archived) : 0;
+ const pendingRetriesResult = requiresFullFailureDetailsSubscriberCount.value > 0 ? getErrorMessagesCount(FailedMessageStatus.RetryIssued) : 0;
+ const disconnectedEndpointsCountResult = getDisconnectedEndpointsCount();
+
+ const [failedMessages, archivedMessages, pendingRetries, disconnectedEndpoints] = await Promise.all([failedMessagesResult, archivedMessagesResult, pendingRetriesResult, disconnectedEndpointsCountResult]);
+
+ failedMessageCount.value = failedMessages;
+ archivedMessageCount.value = archivedMessages;
+ pendingRetriesMessageCount.value = pendingRetries;
+ disconnectedEndpointsCount.value = disconnectedEndpoints;
+ }
+
+ function getErrorMessagesCount(status: FailedMessageStatus) {
+ return fetchAndSetConnectionState(
+ () => serviceControlStore.fetchTypedFromServiceControl(`errors?status=${status}`),
+ connectionState,
+ (response) => parseInt(response.headers.get("Total-Count") ?? "0"),
+ 0
+ );
+ }
+
+ function getDisconnectedEndpointsCount() {
+ return fetchAndSetConnectionState(
+ () => serviceControlStore.fetchTypedFromMonitoring("monitored-endpoints/disconnected"),
+ monitoringConnectionState,
+ (_, data) => {
+ return data;
+ },
+ 0
+ );
+ }
+
+ return {
+ refresh,
+ failedMessageCount,
+ requiresFullFailureDetails,
+ archivedMessageCount,
+ pendingRetriesMessageCount,
+ disconnectedEndpointsCount,
+ connectionState,
+ monitoringConnectionState,
+ displayConnectionsWarning,
+ };
+});
+
+async function fetchAndSetConnectionState(fetchFunction: () => Promise<[Response?, T?]>, connectionState: ConnectionState, action: (response: Response, data: T) => TResult, defaultResult: TResult) {
+ if (connectionState.connecting) {
+ //Skip the connection state checking
+ try {
+ const [response, data] = await fetchFunction();
+ if (response != null && data != null) {
+ return await action(response, data);
+ }
+ } catch (err) {
+ console.log(err);
+ return defaultResult;
+ }
+ }
+ try {
+ if (!connectionState.connected) {
+ connectionState.connecting = true;
+ connectionState.connected = false;
+ }
+
+ try {
+ const [response, data] = await fetchFunction();
+ let result: TResult | null = null;
+ if (response != null && data != null) {
+ result = await action(response, data);
+ }
+ connectionState.unableToConnect = false;
+ connectionState.connectedRecently = true;
+ connectionState.connected = true;
+ connectionState.connecting = false;
+
+ if (result) {
+ return result;
+ }
+ } catch (err) {
+ connectionState.connected = false;
+ connectionState.unableToConnect = true;
+ connectionState.connectedRecently = false;
+ connectionState.connecting = false;
+ console.log(err);
+ }
+ } catch {
+ connectionState.connecting = false;
+ connectionState.connected = false;
+ }
+
+ return defaultResult;
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useConnectionsAndStatsStore, import.meta.hot));
+}
+
+export type ConnectionsAndStatsStore = ReturnType;
diff --git a/frontend/src/stores/CustomChecksStore.ts b/frontend/src/stores/CustomChecksStore.ts
new file mode 100644
index 0000000000..fb02cc2d6f
--- /dev/null
+++ b/frontend/src/stores/CustomChecksStore.ts
@@ -0,0 +1,64 @@
+import CustomCheck from "@/resources/CustomCheck";
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { computed, ref, watch } from "vue";
+import { useCounter } from "@vueuse/core";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export const useCustomChecksStore = defineStore("CustomChecksStore", () => {
+ const prefix = "customchecks/";
+
+ const serviceControlStore = useServiceControlStore();
+
+ const pageNumber = ref(1);
+ const failingCount = ref(0);
+ const failedChecks = ref([]);
+
+ const { count, inc, dec } = useCounter(0);
+ const skipRefresh = computed(() => count.value > 0);
+
+ const refresh = async () => {
+ if (skipRefresh.value) {
+ return;
+ }
+ try {
+ const [response, data] = await serviceControlStore.fetchTypedFromServiceControl(`customchecks?status=fail&page=${pageNumber.value}`);
+ failedChecks.value = data;
+ failingCount.value = parseInt(response.headers.get("Total-Count") ?? "0");
+ } catch (e) {
+ failedChecks.value = [];
+ failingCount.value = 0;
+ throw e;
+ }
+ };
+
+ watch(pageNumber, () => refresh());
+
+ async function dismissCustomCheck(id: string) {
+ try {
+ inc();
+ // NOTE: If it takes more than the refresh interval for ServiceControl to delete the check it will reappear
+ failedChecks.value = failedChecks.value.filter((x) => x.id !== id);
+ failingCount.value--;
+
+ // HINT: This is required to handle the difference between ServiceControl 4 and 5
+ const guid = id.toLocaleLowerCase().startsWith(prefix) ? id.substring(prefix.length) : id;
+ await serviceControlStore.deleteFromServiceControl(`${prefix}${guid}`);
+ } finally {
+ dec();
+ }
+ }
+
+ return {
+ refresh,
+ dismissCustomCheck,
+ pageNumber,
+ failingCount,
+ failedChecks,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useCustomChecksStore, import.meta.hot));
+}
+
+export type CustomChecksStore = ReturnType;
diff --git a/frontend/src/stores/DataContainer.ts b/frontend/src/stores/DataContainer.ts
new file mode 100644
index 0000000000..80a0983e8f
--- /dev/null
+++ b/frontend/src/stores/DataContainer.ts
@@ -0,0 +1,10 @@
+/**
+ * A container for data with loading states.
+ * Used to track loading, error, and not found states for data fetched from APIs.
+ */
+export interface DataContainer {
+ loading?: boolean;
+ failed_to_load?: boolean;
+ not_found?: boolean;
+ data: T;
+}
diff --git a/frontend/src/stores/EditRetryStore.ts b/frontend/src/stores/EditRetryStore.ts
new file mode 100644
index 0000000000..2fe34e8c5b
--- /dev/null
+++ b/frontend/src/stores/EditRetryStore.ts
@@ -0,0 +1,25 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { ref } from "vue";
+import { EditAndRetryConfig } from "@/resources/Configuration";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export const useEditRetryStore = defineStore("EditRetryStore", () => {
+ const config = ref({ enabled: false, locked_headers: [], sensitive_headers: [] });
+ const serviceControlStore = useServiceControlStore();
+
+ async function loadConfig() {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl("edit/config");
+ config.value = data;
+ }
+
+ return {
+ config,
+ loadConfig,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useEditRetryStore, import.meta.hot));
+}
+
+export type EditRetryStore = ReturnType;
diff --git a/frontend/src/stores/EndpointSettingsStore.ts b/frontend/src/stores/EndpointSettingsStore.ts
new file mode 100644
index 0000000000..974a2c16cd
--- /dev/null
+++ b/frontend/src/stores/EndpointSettingsStore.ts
@@ -0,0 +1,29 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { useServiceControlStore } from "./ServiceControlStore";
+import { EndpointSettings } from "@/resources/EndpointSettings";
+import useIsEndpointSettingsSupported from "@/components/heartbeats/isEndpointSettingsSupported";
+
+export const useEndpointSettingsStore = defineStore("EndpointSettingsStore", () => {
+ const defaultEndpointSettingsValue = { name: "", track_instances: true };
+ const serviceControlStore = useServiceControlStore();
+
+ const isEndpointSettingsSupported = useIsEndpointSettingsSupported();
+
+ async function getEndpointSettings(): Promise {
+ if (!isEndpointSettingsSupported.value) return [defaultEndpointSettingsValue];
+
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl(`endpointssettings`);
+ return data;
+ }
+
+ return {
+ defaultEndpointSettingsValue,
+ getEndpointSettings,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useEndpointSettingsStore, import.meta.hot));
+}
+
+export type EndpointSettingsStore = ReturnType;
diff --git a/frontend/src/stores/EnvironmentAndVersionsStore.ts b/frontend/src/stores/EnvironmentAndVersionsStore.ts
new file mode 100644
index 0000000000..0af9a90602
--- /dev/null
+++ b/frontend/src/stores/EnvironmentAndVersionsStore.ts
@@ -0,0 +1,150 @@
+import { isSupported, isUpgradeAvailable } from "@/composables/serviceSemVer";
+import Release from "@/resources/Release";
+import RootUrls from "@/resources/RootUrls";
+import { useMemoize } from "@vueuse/core";
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { computed, reactive } from "vue";
+import { useServiceControlStore } from "./ServiceControlStore";
+import { getDefaultConfig } from "@/defaultConfig";
+
+export const useEnvironmentAndVersionsStore = defineStore("EnvironmentAndVersionsStore", () => {
+ const serviceControlStore = useServiceControlStore();
+
+ const environment = reactive({
+ monitoring_version: "",
+ sc_version: "",
+ minimum_supported_sc_version: "6.6.0",
+ is_compatible_with_sc: true,
+ sp_version: getDefaultConfig().version,
+ supportsArchiveGroups: false,
+ endpoints_error_url: "",
+ known_endpoints_url: "",
+ endpoints_message_search_url: "",
+ endpoints_messages_url: "",
+ endpoints_url: "",
+ errors_url: "",
+ configuration: "",
+ message_search_url: "",
+ sagas_url: "",
+ });
+
+ const newVersions = reactive({
+ newSPVersion: {
+ newspversion: false,
+ newspversionlink: "",
+ newspversionnumber: "",
+ },
+ newSCVersion: {
+ newscversion: false,
+ newscversionlink: "",
+ newscversionnumber: "",
+ },
+ newMVersion: {
+ newmversion: false,
+ newmversionlink: "",
+ newmversionnumber: "",
+ },
+ });
+
+ const serviceControlIsGreaterThan = useMemoize((requiredVersion: string) => computed(() => isSupported(environment.sc_version, requiredVersion)));
+
+ async function refresh() {
+ const productsResult = useServiceProductUrls();
+ const scResult = getPrimaryVersion();
+ const mResult = setMonitoringVersion();
+
+ const [products, scVer] = await Promise.all([productsResult, scResult, mResult]);
+ if (scVer) {
+ environment.supportsArchiveGroups = !!scVer.archived_groups_url;
+ environment.is_compatible_with_sc = isSupported(environment.sc_version, environment.minimum_supported_sc_version);
+ environment.endpoints_error_url = scVer && scVer.endpoints_error_url;
+ environment.known_endpoints_url = scVer && scVer.known_endpoints_url;
+ environment.endpoints_message_search_url = scVer.endpoints_message_search_url;
+ environment.endpoints_messages_url = scVer.endpoints_messages_url;
+ environment.endpoints_url = scVer.endpoints_url;
+ environment.errors_url = scVer.errors_url;
+ environment.configuration = scVer.configuration;
+ environment.message_search_url = scVer.message_search_url;
+ environment.sagas_url = scVer.sagas_url;
+ }
+ if (products.latestSP && isUpgradeAvailable(environment.sp_version, products.latestSP.tag)) {
+ newVersions.newSPVersion.newspversion = true;
+ newVersions.newSPVersion.newspversionlink = products.latestSP.release;
+ newVersions.newSPVersion.newspversionnumber = products.latestSP.tag;
+ }
+ if (products.latestSC && isUpgradeAvailable(environment.sc_version, products.latestSC.tag)) {
+ newVersions.newSCVersion.newscversion = true;
+ newVersions.newSCVersion.newscversionlink = products.latestSC.release;
+ newVersions.newSCVersion.newscversionnumber = products.latestSC.tag;
+ }
+ if (products.latestSC && isUpgradeAvailable(environment.monitoring_version, products.latestSC.tag)) {
+ newVersions.newMVersion.newmversion = true;
+ newVersions.newMVersion.newmversionlink = products.latestSC.release;
+ newVersions.newMVersion.newmversionnumber = products.latestSC.tag;
+ }
+ }
+
+ async function getPrimaryVersion() {
+ try {
+ const [response, data] = await serviceControlStore.fetchTypedFromServiceControl("");
+ environment.sc_version = response.headers.get("X-Particular-Version") ?? "";
+ return data;
+ } catch {
+ return null;
+ }
+ }
+
+ async function setMonitoringVersion() {
+ try {
+ const [response] = await serviceControlStore.fetchTypedFromMonitoring("");
+ if (response) {
+ environment.monitoring_version = response.headers.get("X-Particular-Version") ?? "";
+ }
+ } catch {
+ environment.monitoring_version = "";
+ }
+ }
+
+ return {
+ refresh,
+ environment,
+ newVersions,
+ serviceControlIsGreaterThan,
+ };
+});
+
+async function getData(url: string) {
+ try {
+ const response = await fetch(url);
+ return (await response.json()) as unknown as Release[];
+ } catch (e) {
+ console.log(e);
+ return [
+ {
+ tag: "Unknown",
+ release: "Unknown",
+ published: "Unknown",
+ },
+ ];
+ }
+}
+
+async function useServiceProductUrls() {
+ const spURL = "https://platformupdate.particular.net/servicepulse.txt";
+ const scURL = "https://platformupdate.particular.net/servicecontrol.txt";
+
+ const servicePulse = getData(spURL);
+ const serviceControl = getData(scURL);
+
+ const [sp, sc] = await Promise.all([servicePulse, serviceControl]);
+ const latestSP = sp[0];
+ const latestSC = sc[0];
+
+ return { latestSP, latestSC };
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useEnvironmentAndVersionsStore, import.meta.hot));
+}
+
+export type EnvironmentAndVersionsStore = ReturnType;
diff --git a/frontend/src/stores/HealthChecksStore.ts b/frontend/src/stores/HealthChecksStore.ts
new file mode 100644
index 0000000000..85b642159f
--- /dev/null
+++ b/frontend/src/stores/HealthChecksStore.ts
@@ -0,0 +1,125 @@
+import EmailSettings from "@/components/configuration/EmailSettings";
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { ref } from "vue";
+import { useServiceControlStore } from "./ServiceControlStore";
+import EmailNotifications from "@/resources/EmailNotifications";
+import UpdateEmailNotificationsSettingsRequest from "@/resources/UpdateEmailNotificationsSettingsRequest";
+import { useEnvironmentAndVersionsStore } from "./EnvironmentAndVersionsStore";
+
+export const useHealthChecksStore = defineStore("HealthChecksStore", () => {
+ const emailNotifications = ref({
+ enabled: null,
+ enable_tls: null,
+ smtp_server: "",
+ smtp_port: null,
+ authentication_account: "",
+ authentication_password: "",
+ from: "",
+ to: "",
+ });
+
+ const serviceControlStore = useServiceControlStore();
+ const environmentStore = useEnvironmentAndVersionsStore();
+ const hasResponseStatusInHeaders = environmentStore.serviceControlIsGreaterThan("5.2");
+
+ async function refresh() {
+ let result: EmailNotifications | null = null;
+ try {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl("notifications/email");
+ result = data;
+ } catch (err) {
+ console.error(err);
+ result = {
+ enabled: false,
+ enable_tls: false,
+ };
+ }
+
+ emailNotifications.value = {
+ enabled: result.enabled,
+ enable_tls: result.enable_tls,
+ smtp_server: result.smtp_server ? result.smtp_server : "",
+ smtp_port: result.smtp_port ? result.smtp_port : null,
+ authentication_account: result.authentication_account ? result.authentication_account : "",
+ authentication_password: result.authentication_password ? result.authentication_password : "",
+ from: result.from ? result.from : "",
+ to: result.to ? result.to : "",
+ };
+ }
+
+ async function toggleEmailNotifications() {
+ const result = await getResponseOrError(() =>
+ serviceControlStore.postToServiceControl("notifications/email/toggle", {
+ enabled: !(emailNotifications.value.enabled ?? true),
+ })
+ );
+ if (result.message === "success") return true;
+ else {
+ console.error(result.message);
+ //set it back to what it was
+ emailNotifications.value.enabled = !emailNotifications.value.enabled;
+ return false;
+ }
+ }
+
+ async function testEmailNotifications() {
+ const result = await getResponseOrError(
+ () => serviceControlStore.postToServiceControl("notifications/email/test"),
+ (response) => (hasResponseStatusInHeaders.value ? (response.headers.get("X-Particular-Reason") ?? response.statusText) : response.statusText)
+ );
+ if (result.message === "success") return true;
+ else {
+ console.error(result.message);
+ return false;
+ }
+ }
+
+ async function saveEmailNotifications(newSettings: UpdateEmailNotificationsSettingsRequest) {
+ const result = await getResponseOrError(() => serviceControlStore.postToServiceControl("notifications/email", newSettings));
+ if (result.message === "success") {
+ emailNotifications.value = {
+ enabled: emailNotifications.value.enabled,
+ enable_tls: newSettings.enable_tls,
+ smtp_server: newSettings.smtp_server,
+ smtp_port: newSettings.smtp_port,
+ authentication_account: newSettings.authorization_account,
+ authentication_password: newSettings.authorization_password,
+ from: newSettings.from,
+ to: newSettings.to,
+ };
+ return true;
+ } else {
+ console.error(result.message);
+ return false;
+ }
+ }
+
+ async function getResponseOrError(action: () => Promise, responseStatusTextOverride?: (response: Response) => string) {
+ const responseStatusTextDefault = (response: Response) => response.statusText;
+ const responseStatusText = responseStatusTextOverride ?? responseStatusTextDefault;
+ try {
+ const response = await action();
+ return {
+ message: response.ok ? "success" : `error:${responseStatusText(response)}`,
+ };
+ } catch (err) {
+ return {
+ message: (err as Error).message ?? err,
+ };
+ }
+ }
+
+ return {
+ refresh,
+ emailNotifications,
+ toggleEmailNotifications,
+ saveEmailNotifications,
+ testEmailNotifications,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useHealthChecksStore, import.meta.hot));
+}
+
+export type HealthChecksStore = ReturnType;
diff --git a/frontend/src/stores/HeartbeatInstancesStore.spec.ts b/frontend/src/stores/HeartbeatInstancesStore.spec.ts
new file mode 100644
index 0000000000..b86f029aea
--- /dev/null
+++ b/frontend/src/stores/HeartbeatInstancesStore.spec.ts
@@ -0,0 +1,98 @@
+import { describe, expect, test } from "vitest";
+import { Driver } from "../../test/driver";
+import { makeDriverForTests } from "@component-test-utils";
+import { setActivePinia, storeToRefs } from "pinia";
+import { createTestingPinia } from "@pinia/testing";
+import { ColumnNames, useHeartbeatInstancesStore } from "@/stores/HeartbeatInstancesStore";
+import { EndpointsView } from "@/resources/EndpointView";
+import * as precondition from "../../test/preconditions";
+import { EndpointSettings } from "@/resources/EndpointSettings";
+import { serviceControlWithHeartbeats } from "@/components/heartbeats/serviceControlWithHeartbeats";
+import { EndpointStatus } from "@/resources/Heartbeat";
+import { useEnvironmentAndVersionsStore } from "./EnvironmentAndVersionsStore";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+describe("HeartbeatInstancesStore tests", () => {
+ async function setup(endpoints: EndpointsView[], endpointSettings: EndpointSettings[], preSetup: (driver: Driver) => Promise = () => Promise.resolve()) {
+ const driver = makeDriverForTests();
+ setActivePinia(createTestingPinia({ stubActions: false }));
+
+ await preSetup(driver);
+ await driver.setUp(serviceControlWithHeartbeats);
+ await driver.setUp(precondition.hasEndpointSettings(endpointSettings));
+ await driver.setUp(precondition.hasHeartbeatsEndpoints(endpoints));
+
+ useServiceControlStore();
+ await useEnvironmentAndVersionsStore().refresh();
+
+ const store = useHeartbeatInstancesStore();
+ const refs = storeToRefs(store);
+
+ await store.refresh();
+
+ return { driver, ...refs };
+ }
+
+ test("no endpoints", async () => {
+ const { filteredInstances } = await setup([], []);
+
+ expect(filteredInstances.value.length).toBe(0);
+ });
+
+ test("filter by name", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: false,
+ id: "",
+ name: "",
+ monitor_heartbeat: false,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { filteredInstances, instanceFilterString } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ host_display_name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ host_display_name: "johnny" }) },
+ { ...defaultEndpointsView, ...(>{ host_display_name: "Oliver" }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(filteredInstances.value.length).toBe(3);
+ instanceFilterString.value = "John";
+ expect(filteredInstances.value.length).toBe(2);
+ instanceFilterString.value = "Oliver";
+ expect(filteredInstances.value.length).toBe(1);
+ });
+
+ test("sort by", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: false,
+ id: "",
+ name: "",
+ monitor_heartbeat: false,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { filteredInstances, sortByInstances } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ host_display_name: "John", heartbeat_information: { last_report_at: "2024-10-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ host_display_name: "Anna", heartbeat_information: { last_report_at: "2024-01-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ host_display_name: "Oliver", heartbeat_information: { last_report_at: "2024-06-01T00:00:00" } }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ const names = () => filteredInstances.value.map((value) => value.host_display_name);
+ sortByInstances.value = { property: ColumnNames.InstanceName, isAscending: true };
+ expect(names()).toEqual(["Anna", "John", "Oliver"]);
+
+ sortByInstances.value = { property: ColumnNames.InstanceName, isAscending: false };
+ expect(names()).toEqual(["Oliver", "John", "Anna"]);
+
+ sortByInstances.value = { property: ColumnNames.LastHeartbeat, isAscending: true };
+ expect(names()).toEqual(["Anna", "Oliver", "John"]);
+
+ sortByInstances.value = { property: ColumnNames.LastHeartbeat, isAscending: false };
+ expect(names()).toEqual(["John", "Oliver", "Anna"]);
+ });
+});
diff --git a/frontend/src/stores/HeartbeatInstancesStore.ts b/frontend/src/stores/HeartbeatInstancesStore.ts
new file mode 100644
index 0000000000..d595b759f4
--- /dev/null
+++ b/frontend/src/stores/HeartbeatInstancesStore.ts
@@ -0,0 +1,72 @@
+import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia";
+import { computed, ref, watch } from "vue";
+import moment from "moment";
+import type { SortInfo } from "@/components/SortInfo";
+import { type GroupPropertyType, SortDirection } from "@/resources/SortOptions";
+import getSortFunction from "@/components/getSortFunction";
+import { useHeartbeatsStore } from "@/stores/HeartbeatsStore";
+import { EndpointsView } from "@/resources/EndpointView";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export enum ColumnNames {
+ InstanceName = "name",
+ LastHeartbeat = "latestHeartbeat",
+ MuteToggle = "toggleMuteAlerts",
+}
+
+const columnSortings = new Map GroupPropertyType>([
+ [ColumnNames.InstanceName, (endpoint) => endpoint.host_display_name],
+ [ColumnNames.LastHeartbeat, (endpoint) => moment.utc(endpoint.heartbeat_information?.last_report_at ?? "1975-01-01T00:00:00")],
+ [ColumnNames.MuteToggle, (endpoint) => !endpoint.monitor_heartbeat],
+]);
+
+export const useHeartbeatInstancesStore = defineStore("HeartbeatInstancesStore", () => {
+ const serviceControlStore = useServiceControlStore();
+
+ const instanceFilterString = ref("");
+ const store = useHeartbeatsStore();
+ const { endpointInstances } = storeToRefs(store);
+ const sortByInstances = ref({
+ property: ColumnNames.InstanceName,
+ isAscending: true,
+ });
+
+ const sortedInstances = computed(() => endpointInstances.value.sort(getSortFunction(columnSortings.get(sortByInstances.value.property), sortByInstances.value.isAscending ? SortDirection.Ascending : SortDirection.Descending)));
+ const filteredInstances = computed(() => sortedInstances.value.filter((instance) => !instanceFilterString.value || instance.host_display_name.toLowerCase().includes(instanceFilterString.value.toLowerCase())));
+
+ const refresh = () => store.refresh();
+
+ watch(instanceFilterString, (newValue) => {
+ setInstanceFilterString(newValue);
+ });
+
+ function setInstanceFilterString(filter: string) {
+ instanceFilterString.value = filter;
+ }
+
+ async function deleteEndpointInstance(endpoint: EndpointsView) {
+ await serviceControlStore.deleteFromServiceControl(`endpoints/${endpoint.id}`);
+ await store.refresh();
+ }
+
+ async function toggleEndpointMonitor(endpoints: EndpointsView[]) {
+ await Promise.all(endpoints.map((endpoint) => serviceControlStore.patchToServiceControl(`endpoints/${endpoint.id}`, { monitor_heartbeat: !endpoint.monitor_heartbeat })));
+ await store.refresh();
+ }
+
+ return {
+ refresh,
+ sortedInstances,
+ filteredInstances,
+ instanceFilterString,
+ deleteEndpointInstance,
+ toggleEndpointMonitor,
+ sortByInstances,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useHeartbeatInstancesStore, import.meta.hot));
+}
+
+export type HeartbeatInstancesStore = ReturnType;
diff --git a/frontend/src/stores/HeartbeatsStore.spec.ts b/frontend/src/stores/HeartbeatsStore.spec.ts
new file mode 100644
index 0000000000..eadfae06af
--- /dev/null
+++ b/frontend/src/stores/HeartbeatsStore.spec.ts
@@ -0,0 +1,428 @@
+import { describe, expect, test } from "vitest";
+import { Driver } from "../../test/driver";
+import { makeDriverForTests } from "@component-test-utils";
+import { setActivePinia, storeToRefs } from "pinia";
+import { createTestingPinia } from "@pinia/testing";
+import { EndpointsView } from "@/resources/EndpointView";
+import * as precondition from "../../test/preconditions";
+import { EndpointSettings } from "@/resources/EndpointSettings";
+import { serviceControlWithHeartbeats } from "@/components/heartbeats/serviceControlWithHeartbeats";
+import { EndpointStatus } from "@/resources/Heartbeat";
+import { ColumnNames, useHeartbeatsStore } from "@/stores/HeartbeatsStore";
+import { useEnvironmentAndVersionsStore } from "./EnvironmentAndVersionsStore";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+describe("HeartbeatsStore tests", () => {
+ async function setup(endpoints: EndpointsView[], endpointSettings: EndpointSettings[] = [{ name: "", track_instances: true }], preSetup: (driver: Driver) => Promise = () => Promise.resolve()) {
+ const driver = makeDriverForTests();
+ setActivePinia(createTestingPinia({ stubActions: false }));
+
+ await preSetup(driver);
+ await driver.setUp(serviceControlWithHeartbeats);
+ await driver.setUp(precondition.hasHeartbeatsEndpoints(endpoints, endpointSettings));
+
+ useServiceControlStore();
+ await useEnvironmentAndVersionsStore().refresh();
+
+ const store = useHeartbeatsStore();
+ const storeRefs = storeToRefs(store);
+ await store.refresh();
+
+ return { driver, ...store, ...storeRefs };
+ }
+
+ test("no endpoints", async () => {
+ const { filteredHealthyEndpoints, filteredUnhealthyEndpoints } = await setup([]);
+
+ expect(filteredHealthyEndpoints.value.length).toBe(0);
+ expect(filteredUnhealthyEndpoints.value.length).toBe(0);
+ });
+
+ describe("healthchecks total", () => {
+ test("all heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { failedHeartbeatsCount } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica" }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(failedHeartbeatsCount.value).toBe(0);
+ });
+
+ test("some not heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { failedHeartbeatsCount } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(failedHeartbeatsCount.value).toBe(2);
+ });
+
+ test("some not heart beating with no tracking", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { failedHeartbeatsCount } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [
+ { name: "", track_instances: true },
+ { name: "John", track_instances: false },
+ ]
+ );
+
+ expect(failedHeartbeatsCount.value).toBe(2);
+ });
+
+ test("some not heart beating in same logical endpoint and tracking", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { failedHeartbeatsCount } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(failedHeartbeatsCount.value).toBe(3);
+ });
+
+ test("all instances muted", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { failedHeartbeatsCount } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [
+ { name: "", track_instances: true },
+ { name: "John", track_instances: false },
+ ]
+ );
+
+ expect(failedHeartbeatsCount.value).toBe(1);
+ });
+ });
+
+ describe("healthy endpoints", () => {
+ describe("total number when tracking instances", () => {
+ test("when all instances are heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { healthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(healthyEndpoints.value.length).toBe(1);
+ });
+
+ test("when some instances are not heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { healthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(healthyEndpoints.value.length).toBe(0);
+ });
+ });
+
+ describe("total number when not tracking instances", () => {
+ test("when all instances are heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { healthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false }) },
+ ],
+ [{ name: "", track_instances: false }]
+ );
+
+ expect(healthyEndpoints.value.length).toBe(1);
+ });
+
+ test("when some instances are not heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { healthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [{ name: "", track_instances: false }]
+ );
+
+ expect(healthyEndpoints.value.length).toBe(0);
+ });
+ });
+
+ test("filter by name", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { filteredHealthyEndpoints, endpointFilterString } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "johnny" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver" }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(filteredHealthyEndpoints.value.length).toBe(3);
+ endpointFilterString.value = "John";
+ expect(filteredHealthyEndpoints.value.length).toBe(2);
+ endpointFilterString.value = "Oliver";
+ expect(filteredHealthyEndpoints.value.length).toBe(1);
+ });
+
+ test("sort by", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { filteredHealthyEndpoints, sortByInstances } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John", heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "2024-10-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "2024-10-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Anna", heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "2024-01-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Anna", heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "2024-01-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Anna", heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "2024-01-01T00:00:00" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "2024-06-01T00:00:00" } }) },
+ ],
+ [
+ { name: "", track_instances: true },
+ { name: "John", track_instances: false },
+ ]
+ );
+
+ const names = () => filteredHealthyEndpoints.value.map((value) => value.name);
+
+ sortByInstances.value = { property: ColumnNames.Name, isAscending: true };
+ expect(names()).toEqual(["Anna", "John", "Oliver"]);
+
+ sortByInstances.value = { property: ColumnNames.Name, isAscending: false };
+ expect(names()).toEqual(["Oliver", "John", "Anna"]);
+
+ sortByInstances.value = { property: ColumnNames.LastHeartbeat, isAscending: true };
+ expect(names()).toEqual(["Anna", "Oliver", "John"]);
+
+ sortByInstances.value = { property: ColumnNames.LastHeartbeat, isAscending: false };
+ expect(names()).toEqual(["John", "Oliver", "Anna"]);
+
+ sortByInstances.value = { property: ColumnNames.Tracked, isAscending: true };
+ expect(names()[0]).toBe("John");
+
+ sortByInstances.value = { property: ColumnNames.Tracked, isAscending: false };
+ expect(names()[2]).toBe("John");
+
+ sortByInstances.value = { property: ColumnNames.InstancesTotal, isAscending: true };
+ expect(names()[2]).toBe("Anna");
+
+ sortByInstances.value = { property: ColumnNames.InstancesTotal, isAscending: false };
+ expect(names()[0]).toBe("Anna");
+ });
+ });
+
+ describe("unhealthy endpoints", () => {
+ describe("total number when tracking instances", () => {
+ test("when all instances are heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: false,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { unhealthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(unhealthyEndpoints.value.length).toBe(2);
+ });
+
+ test("when some instances are not heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: false,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { unhealthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [{ name: "", track_instances: true }]
+ );
+
+ expect(unhealthyEndpoints.value.length).toBe(3);
+ });
+ });
+
+ describe("total number when not tracking instances", () => {
+ test("when all instances are heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: true,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { unhealthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false }) },
+ ],
+ [{ name: "", track_instances: false }]
+ );
+
+ expect(unhealthyEndpoints.value.length).toBe(2);
+ });
+
+ test("when some instances are not heart beating", async () => {
+ const defaultEndpointsView = {
+ is_sending_heartbeats: false,
+ id: "",
+ name: "",
+ monitor_heartbeat: true,
+ host_display_name: "",
+ heartbeat_information: { reported_status: EndpointStatus.Alive, last_report_at: "" },
+ };
+ const { unhealthyEndpoints } = await setup(
+ [
+ { ...defaultEndpointsView, ...(>{ name: "Henry" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John" }) },
+ { ...defaultEndpointsView, ...(>{ name: "John", monitor_heartbeat: false }) },
+ { ...defaultEndpointsView, ...(>{ name: "Oliver", heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica" }) },
+ { ...defaultEndpointsView, ...(>{ name: "Monica", monitor_heartbeat: false, heartbeat_information: { reported_status: EndpointStatus.Dead, last_report_at: "" } }) },
+ ],
+ [{ name: "", track_instances: false }]
+ );
+
+ expect(unhealthyEndpoints.value.length).toBe(3);
+ });
+ });
+ });
+});
diff --git a/frontend/src/stores/HeartbeatsStore.ts b/frontend/src/stores/HeartbeatsStore.ts
new file mode 100644
index 0000000000..af808ee6ab
--- /dev/null
+++ b/frontend/src/stores/HeartbeatsStore.ts
@@ -0,0 +1,197 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { computed, ref, watch } from "vue";
+import { EndpointStatus, LogicalEndpoint } from "@/resources/Heartbeat";
+import moment from "moment";
+import { SortDirection, type GroupPropertyType } from "@/resources/SortOptions";
+import getSortFunction from "@/components/getSortFunction";
+import { EndpointsView } from "@/resources/EndpointView";
+import type { SortInfo } from "@/components/SortInfo";
+import { EndpointSettings } from "@/resources/EndpointSettings";
+import { useServiceControlStore } from "./ServiceControlStore";
+import { useEndpointSettingsStore } from "./EndpointSettingsStore";
+
+export enum ColumnNames {
+ Name = "name",
+ InstancesDown = "instancesDown",
+ InstancesTotal = "instancesTotal",
+ LastHeartbeat = "latestHeartbeat",
+ Muted = "muted",
+ Tracked = "instancesTracked",
+ TrackToggle = "toggleInstancesTracked",
+}
+
+export enum MutedType {
+ None = 0,
+ Some = 1,
+ All = 2,
+}
+
+const columnSortings = new Map GroupPropertyType>([
+ [ColumnNames.Name, (endpoint) => endpoint.name],
+ [ColumnNames.InstancesDown, (endpoint) => endpoint.alive_count - endpoint.down_count],
+ [ColumnNames.InstancesTotal, (endpoint) => endpoint.alive_count + endpoint.down_count],
+ [ColumnNames.LastHeartbeat, (endpoint) => moment.utc(endpoint.heartbeat_information?.last_report_at ?? "1975-01-01T00:00:00")],
+ [
+ ColumnNames.Muted,
+ (endpoint) => {
+ switch (endpoint.muted_count) {
+ case 0:
+ return MutedType.None;
+ case endpoint.alive_count + endpoint.down_count:
+ return MutedType.All;
+ default:
+ return MutedType.Some;
+ }
+ },
+ ],
+ [ColumnNames.Tracked, (endpoint) => endpoint.track_instances],
+ [ColumnNames.TrackToggle, (endpoint) => endpoint.track_instances],
+]);
+
+export const useHeartbeatsStore = defineStore("HeartbeatsStore", () => {
+ const serviceControlStore = useServiceControlStore();
+ const endpointSettingsStore = useEndpointSettingsStore();
+
+ const sortByInstances = ref({
+ property: ColumnNames.Name,
+ isAscending: true,
+ });
+
+ const defaultTrackingInstancesValue = ref(endpointSettingsStore.defaultEndpointSettingsValue.track_instances);
+ const endpointFilterString = ref("");
+ const itemsPerPage = ref(20);
+ const endpointInstances = ref([]);
+ const settings = ref([]);
+ const sortedEndpoints = computed(() =>
+ mapEndpointsToLogical(endpointInstances.value, settings.value).sort(getSortFunction(columnSortings.get(sortByInstances.value.property), sortByInstances.value.isAscending ? SortDirection.Ascending : SortDirection.Descending))
+ );
+ const filteredEndpoints = computed(() => sortedEndpoints.value.filter((endpoint) => !endpointFilterString.value || endpoint.name.toLowerCase().includes(endpointFilterString.value.toLowerCase())));
+ const healthyEndpoints = computed(() =>
+ sortedEndpoints.value.filter(function (endpoint) {
+ return endpoint.monitor_heartbeat && endpoint.heartbeat_information?.reported_status === EndpointStatus.Alive && ((endpoint.track_instances && endpoint.down_count === 0) || (!endpoint.track_instances && endpoint.alive_count > 0));
+ })
+ );
+ const filteredHealthyEndpoints = computed(() => healthyEndpoints.value.filter((endpoint) => !endpointFilterString.value || endpoint.name.toLowerCase().includes(endpointFilterString.value.toLowerCase())));
+ const unhealthyEndpoints = computed(() =>
+ sortedEndpoints.value.filter(function (endpoint) {
+ return !endpoint.monitor_heartbeat || endpoint.heartbeat_information?.reported_status === EndpointStatus.Dead || (endpoint.track_instances && endpoint.down_count > 0) || (!endpoint.track_instances && endpoint.alive_count === 0);
+ })
+ );
+ const filteredUnhealthyEndpoints = computed(() => unhealthyEndpoints.value.filter((endpoint) => !endpointFilterString.value || endpoint.name.toLowerCase().includes(endpointFilterString.value.toLowerCase())));
+ const failedHeartbeatsCount = computed(() => {
+ let counter = 0;
+
+ for (const logical of sortedEndpoints.value) {
+ const endpointInstancesThatAreNotMuted = endpointInstances.value.filter((instance) => instance.name === logical.name && instance.monitor_heartbeat);
+
+ if (logical.track_instances) {
+ if (endpointInstancesThatAreNotMuted.some((instance) => instance.heartbeat_information?.reported_status !== EndpointStatus.Alive)) {
+ counter++;
+ }
+ } else {
+ if (!endpointInstancesThatAreNotMuted.some((instance) => instance.heartbeat_information?.reported_status === EndpointStatus.Alive)) {
+ counter++;
+ }
+ }
+ }
+
+ return counter;
+ });
+ watch(endpointFilterString, (newValue) => {
+ setEndpointFilterString(newValue);
+ });
+
+ const refresh = async () => {
+ try {
+ const [[, data], data2] = await Promise.all([serviceControlStore.fetchTypedFromServiceControl("endpoints"), endpointSettingsStore.getEndpointSettings()]);
+ endpointInstances.value = data;
+ settings.value = data2;
+ defaultTrackingInstancesValue.value = data2.find((value) => value.name === "")!.track_instances;
+ } catch (e) {
+ endpointInstances.value = settings.value = [];
+ throw e;
+ }
+ };
+
+ async function updateEndpointSettings(endpoints: Pick[]) {
+ await Promise.all(endpoints.map((endpoint) => serviceControlStore.patchToServiceControl(`endpointssettings/${endpoint.name}`, { track_instances: !endpoint.track_instances })));
+ await refresh();
+ }
+
+ function instanceDisplayText(endpoint: LogicalEndpoint) {
+ const total = endpoint.alive_count + endpoint.down_count;
+
+ if (endpoint.track_instances) {
+ return `${endpoint.alive_count}/${total}`;
+ } else {
+ return `${endpoint.alive_count}`;
+ }
+ }
+
+ function setEndpointFilterString(filter: string) {
+ endpointFilterString.value = filter;
+ }
+
+ function setItemsPerPage(value: number) {
+ itemsPerPage.value = value;
+ }
+
+ function mapEndpointsToLogical(endpoints: EndpointsView[], settings: EndpointSettings[]): LogicalEndpoint[] {
+ const logicalNames = [...new Set(endpoints.map((endpoint) => endpoint.name))];
+
+ return logicalNames.map((endpointName) => {
+ const endpointInstances = endpoints.filter((endpoint) => endpoint.name === endpointName);
+ const aliveList = endpointInstances.filter((endpoint) => endpoint.heartbeat_information && endpoint.heartbeat_information.reported_status === EndpointStatus.Alive);
+
+ const aliveCount = aliveList.length;
+ const downCount = endpointInstances.length - aliveCount;
+
+ return {
+ id: endpointName, //need this to be consistent between data refreshes for UI purposes, so using name rather than an id from one of the instances
+ name: endpointName,
+ alive_count: aliveCount,
+ down_count: downCount,
+ muted_count: endpointInstances.filter((endpoint) => !endpoint.monitor_heartbeat).length,
+ track_instances: settings.find((value) => value.name === endpointName)?.track_instances ?? defaultTrackingInstancesValue.value,
+ heartbeat_information: {
+ reported_status: aliveCount > 0 ? EndpointStatus.Alive : EndpointStatus.Dead,
+ last_report_at: endpointInstances.reduce((previousMax: EndpointsView | null, endpoint: EndpointsView) => {
+ if (endpoint.heartbeat_information) {
+ if (previousMax) {
+ return moment.utc(endpoint.heartbeat_information.last_report_at) > moment.utc(previousMax.heartbeat_information!.last_report_at) ? endpoint : previousMax;
+ }
+ return endpoint;
+ }
+ return previousMax;
+ }, null)?.heartbeat_information?.last_report_at,
+ },
+ monitor_heartbeat: endpointInstances.every((endpoint) => endpoint.monitor_heartbeat),
+ } as LogicalEndpoint;
+ });
+ }
+
+ return {
+ refresh,
+ defaultTrackingInstancesValue,
+ updateEndpointSettings,
+ sortedEndpoints,
+ filteredEndpoints,
+ endpointInstances,
+ healthyEndpoints,
+ filteredHealthyEndpoints,
+ unhealthyEndpoints,
+ filteredUnhealthyEndpoints,
+ failedHeartbeatsCount,
+ instanceDisplayText,
+ sortByInstances,
+ endpointFilterString,
+ itemsPerPage,
+ setItemsPerPage,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useHeartbeatsStore, import.meta.hot));
+}
+
+export type HeartbeatsStore = ReturnType;
diff --git a/frontend/src/stores/LicenseStore.ts b/frontend/src/stores/LicenseStore.ts
new file mode 100644
index 0000000000..296ac3f251
--- /dev/null
+++ b/frontend/src/stores/LicenseStore.ts
@@ -0,0 +1,184 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { computed, reactive, ref } from "vue";
+import { useServiceControlStore } from "./ServiceControlStore";
+import LicenseInfo, { LicenseStatus } from "@/resources/LicenseInfo";
+import { LicenseWarningLevel } from "@/composables/LicenseStatus";
+import { useGetDayDiffFromToday } from "@/composables/formatter";
+
+export const useLicenseStore = defineStore("LicenseStore", () => {
+ const serviceControlStore = useServiceControlStore();
+
+ const license = reactive({
+ edition: "",
+ expiration_date: "",
+ upgrade_protection_expiration: "",
+ license_type: "",
+ instance_name: "",
+ trial_license: true,
+ registered_to: "",
+ status: "",
+ license_status: LicenseStatus.Unavailable,
+ license_extension_url: "",
+ });
+
+ const licenseStatus = reactive({
+ isSubscriptionLicense: false,
+ isUpgradeProtectionLicense: false,
+ isTrialLicense: false,
+ isPlatformExpired: false,
+ isPlatformTrialExpired: false,
+ isPlatformTrialExpiring: false,
+ isInvalidDueToUpgradeProtectionExpired: false,
+ isValidWithExpiredUpgradeProtection: false,
+ isValidWithExpiringUpgradeProtection: false,
+ isExpired: false,
+ upgradeDaysLeft: "",
+ subscriptionDaysLeft: "",
+ trialDaysLeft: "",
+ warningLevel: LicenseWarningLevel.None,
+ licenseExtensionUrl: "",
+ });
+
+ const loading = ref(false);
+
+ // Computed properties for license formatting
+ const licenseEdition = computed(() => {
+ return `${license.license_type}${license.edition ? `, ${license.edition}` : ""}`;
+ });
+
+ const formattedInstanceName = computed(() => {
+ return license.instance_name || "Upgrade ServiceControl to v3.4.0+ to see more information about this license";
+ });
+
+ const formattedExpirationDate = computed(() => {
+ return license.expiration_date ? new Date(license.expiration_date.replace("Z", "")).toLocaleDateString() : "";
+ });
+
+ const formattedUpgradeProtectionExpiration = computed(() => {
+ return license.upgrade_protection_expiration ? new Date(license.upgrade_protection_expiration.replace("Z", "")).toLocaleDateString() : "";
+ });
+
+ async function refresh() {
+ loading.value = true;
+ try {
+ const lic = await getLicense();
+ if (lic === null) {
+ return;
+ }
+ license.license_type = lic.license_type;
+ license.expiration_date = lic.expiration_date;
+ license.trial_license = lic.trial_license;
+ license.edition = lic.edition;
+ license.license_status = lic.license_status;
+ license.instance_name = lic.instance_name;
+ license.registered_to = lic.registered_to;
+ license.status = lic.status;
+ license.license_extension_url = lic.license_extension_url ?? "https://particular.net/extend-your-trial?p=servicepulse";
+ license.upgrade_protection_expiration = lic.upgrade_protection_expiration;
+
+ licenseStatus.isSubscriptionLicense = isSubscriptionLicense();
+ licenseStatus.isUpgradeProtectionLicense = isUpgradeProtectionLicense();
+ licenseStatus.isTrialLicense = license.trial_license;
+ licenseStatus.isPlatformExpired = license.license_status === LicenseStatus.InvalidDueToExpiredSubscription;
+ licenseStatus.isPlatformTrialExpiring = license.license_status === LicenseStatus.ValidWithExpiringTrial;
+ licenseStatus.isPlatformTrialExpired = license.license_status === LicenseStatus.InvalidDueToExpiredTrial;
+ licenseStatus.isInvalidDueToUpgradeProtectionExpired = license.license_status === LicenseStatus.InvalidDueToExpiredUpgradeProtection;
+ licenseStatus.isValidWithExpiredUpgradeProtection = license.license_status === LicenseStatus.ValidWithExpiredUpgradeProtection;
+ licenseStatus.isValidWithExpiringUpgradeProtection = license.license_status === LicenseStatus.ValidWithExpiringUpgradeProtection;
+ licenseStatus.upgradeDaysLeft = getUpgradeDaysLeft();
+ licenseStatus.subscriptionDaysLeft = getSubscriptionDaysLeft();
+ licenseStatus.trialDaysLeft = getTrialDaysLeft();
+ licenseStatus.warningLevel = getLicenseWarningLevel();
+ licenseStatus.isExpired = licenseStatus.isPlatformExpired || licenseStatus.isPlatformTrialExpired || licenseStatus.isInvalidDueToUpgradeProtectionExpired;
+ licenseStatus.licenseExtensionUrl = license.license_extension_url;
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function getLicense() {
+ try {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl("license?refresh=true&clientName=servicepulse");
+ return data;
+ } catch (err) {
+ console.error("Error fetching license information", err);
+ return null;
+ }
+ }
+
+ function getLicenseWarningLevel() {
+ switch (license.license_status) {
+ case LicenseStatus.InvalidDueToExpiredTrial:
+ case LicenseStatus.InvalidDueToExpiredSubscription:
+ case LicenseStatus.InvalidDueToExpiredUpgradeProtection:
+ return LicenseWarningLevel.Danger;
+ case LicenseStatus.ValidWithExpiringUpgradeProtection:
+ case LicenseStatus.ValidWithExpiringTrial:
+ case LicenseStatus.ValidWithExpiredUpgradeProtection:
+ case LicenseStatus.ValidWithExpiringSubscription:
+ return LicenseWarningLevel.Warning;
+ default:
+ return LicenseWarningLevel.None;
+ }
+ }
+
+ function isUpgradeProtectionLicense() {
+ return license.upgrade_protection_expiration !== undefined && license.upgrade_protection_expiration !== "";
+ }
+
+ function isSubscriptionLicense() {
+ return license.expiration_date !== undefined && license.expiration_date !== "" && !license.trial_license;
+ }
+
+ function getSubscriptionDaysLeft() {
+ if (license.license_status === LicenseStatus.InvalidDueToExpiredSubscription) return " - expired";
+
+ const isExpiring = license.license_status === LicenseStatus.ValidWithExpiringSubscription;
+ return getExpiringText(isExpiring, license.expiration_date);
+ }
+
+ function getTrialDaysLeft() {
+ if (license.license_status === LicenseStatus.InvalidDueToExpiredTrial) return " - expired";
+
+ const isExpiring = license.license_status === LicenseStatus.ValidWithExpiringTrial;
+ return getExpiringText(isExpiring, license.expiration_date);
+ }
+
+ function getExpiringText(isExpiring: boolean, expirationDate: string) {
+ const expiringIn = useGetDayDiffFromToday(expirationDate);
+ if (isNaN(expiringIn)) return "";
+ if (!isExpiring) return ` - ${expiringIn} days left`;
+ if (expiringIn === 0) return " - expiring today";
+ if (expiringIn === 1) return " - expiring tomorrow";
+ return ` - expiring in ${expiringIn} days`;
+ }
+
+ function getUpgradeDaysLeft() {
+ if (license.license_status === LicenseStatus.InvalidDueToExpiredUpgradeProtection) return " - expired";
+
+ const expiringIn = useGetDayDiffFromToday(license.upgrade_protection_expiration);
+ //TODO: can this be unified with the function above? Text is currently similar but not identical.
+ if (isNaN(expiringIn)) return "";
+ if (expiringIn <= 0) return " - expired";
+ if (expiringIn === 0) return " - expiring today";
+ if (expiringIn === 1) return " - 1 day left";
+ return " - " + expiringIn + " days left";
+ }
+
+ return {
+ refresh,
+ license,
+ licenseStatus,
+ loading,
+ licenseEdition,
+ formattedInstanceName,
+ formattedExpirationDate,
+ formattedUpgradeProtectionExpiration,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useLicenseStore, import.meta.hot));
+}
+
+export type LicenseStore = ReturnType;
diff --git a/frontend/src/stores/MessageStore.ts b/frontend/src/stores/MessageStore.ts
new file mode 100644
index 0000000000..9c8aa06454
--- /dev/null
+++ b/frontend/src/stores/MessageStore.ts
@@ -0,0 +1,381 @@
+import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia";
+import { computed, reactive, Ref, ref } from "vue";
+import Header from "@/resources/Header";
+import type EndpointDetails from "@/resources/EndpointDetails";
+import { FailedMessage, ExceptionDetails, FailedMessageStatus } from "@/resources/FailedMessage";
+import { useEditRetryStore } from "@/stores/EditRetryStore";
+import { useConfigurationStore } from "@/stores/ConfigurationStore";
+import Message, { MessageStatus } from "@/resources/Message";
+import moment from "moment/moment";
+import { parse, stringify } from "lossless-json";
+import xmlFormat from "xml-formatter";
+import { DataContainer } from "./DataContainer";
+import { useServiceControlStore } from "./ServiceControlStore";
+import EditRetryResponse from "@/resources/EditRetryResponse";
+import { EditedMessage } from "@/resources/EditMessage";
+import useEnvironmentAndVersionsAutoRefresh from "@/composables/useEnvironmentAndVersionsAutoRefresh";
+
+interface Model {
+ id?: string;
+ message_id?: string;
+ conversation_id?: string;
+ message_type?: string;
+ sending_endpoint?: EndpointDetails;
+ receiving_endpoint?: EndpointDetails;
+ body_url?: string;
+ status?: MessageStatus;
+ processed_at?: string;
+ failure_status: Partial<{
+ retried: boolean;
+ archiving: boolean;
+ restoring: boolean;
+ archived: boolean;
+ resolved: boolean;
+ delete_soon: boolean;
+ retry_in_progress: boolean;
+ delete_in_progress: boolean;
+ restore_in_progress: boolean;
+ submitted_for_retrial: boolean;
+ }>;
+ failure_metadata: Partial<{
+ exception: ExceptionDetails;
+ number_of_processing_attempts: number;
+ status: FailedMessageStatus;
+ time_of_failure: string;
+ last_modified: string;
+ edited: boolean;
+ edit_of: string;
+ deleted_in: string;
+ redirect: boolean;
+ }>;
+ dialog_status: Partial<{
+ show_delete_confirm: boolean;
+ show_restore_confirm: boolean;
+ show_retry_confirm: boolean;
+ show_edit_retry_modal: boolean;
+ }>;
+ invoked_saga: Partial<{
+ has_saga: boolean;
+ saga_id: string;
+ saga_type: string;
+ }>;
+}
+
+export const useMessageStore = defineStore("MessageStore", () => {
+ const headers = ref>({ data: [] });
+ const body = ref>({ data: {} });
+ const state = reactive>({ data: { failure_metadata: {}, failure_status: {}, dialog_status: {}, invoked_saga: {} } });
+ const editRetryResponse = ref(null);
+ let bodyLoadedId = "";
+ let conversationLoadedId = "";
+ const conversationData = ref>({ data: [] });
+ const editRetryStore = useEditRetryStore();
+ const configStore = useConfigurationStore();
+ const serviceControlStore = useServiceControlStore();
+ const { store: environmentStore } = useEnvironmentAndVersionsAutoRefresh();
+ const areSimpleHeadersSupported = environmentStore.serviceControlIsGreaterThan("5.2.0");
+
+ const { config: edit_and_retry_config } = storeToRefs(editRetryStore);
+ const { configuration } = storeToRefs(configStore);
+ const error_retention_period = computed(() => moment.duration(configuration.value?.data_retention?.error_retention_period).asHours());
+
+ // eslint-disable-next-line promise/catch-or-return,promise/prefer-await-to-then,promise/valid-params
+ Promise.all([editRetryStore.loadConfig(), configStore.refresh()]).then();
+
+ function reset() {
+ state.data = { failure_metadata: {}, failure_status: {}, dialog_status: {}, invoked_saga: {} };
+ headers.value.data = [];
+ body.value.data = { value: "", content_type: "" };
+ bodyLoadedId = "";
+ conversationLoadedId = "";
+ conversationData.value.data = [];
+ editRetryResponse.value = null;
+ }
+
+ async function loadFailedMessage(id: string) {
+ state.loading = true;
+ state.failed_to_load = false;
+ state.not_found = false;
+
+ try {
+ const response = await serviceControlStore.fetchFromServiceControl(`errors/last/${id}`);
+ if (response.status === 404) {
+ state.not_found = true;
+ return;
+ } else if (!response.ok) {
+ state.failed_to_load = true;
+ return;
+ }
+
+ const message = (await response.json()) as FailedMessage;
+ state.data.message_id = message.message_id;
+ state.data.message_type = message.message_type;
+ state.data.sending_endpoint = message.sending_endpoint;
+ state.data.receiving_endpoint = message.receiving_endpoint;
+ state.data.failure_status.archived = message.status === FailedMessageStatus.Archived;
+ state.data.failure_status.resolved = message.status === FailedMessageStatus.Resolved;
+ state.data.failure_status.retried = message.status === FailedMessageStatus.RetryIssued;
+ state.data.failure_metadata.last_modified = message.last_modified;
+ state.data.failure_metadata.exception = message.exception;
+ state.data.failure_metadata.time_of_failure = message.time_of_failure;
+ state.data.failure_metadata.edited = message.edited;
+ state.data.failure_metadata.edit_of = message.edit_of;
+ state.data.failure_metadata.number_of_processing_attempts = message.number_of_processing_attempts;
+ state.data.failure_metadata.status = message.status;
+
+ await loadMessage(state.data.message_id, id);
+ } catch {
+ state.failed_to_load = true;
+ return;
+ } finally {
+ state.loading = false;
+ }
+
+ const countdown = moment(state.data.failure_metadata.last_modified).add(error_retention_period.value, "hours");
+ state.data.failure_status.delete_soon = countdown < moment();
+ state.data.failure_metadata.deleted_in = countdown.format();
+ }
+
+ async function loadMessage(messageId: string, id: string) {
+ state.data.id = id;
+ state.loading = headers.value.loading = true;
+ state.failed_to_load = headers.value.failed_to_load = false;
+ state.not_found = headers.value.not_found = false;
+
+ try {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl(`messages/search/${messageId}`);
+
+ const message = data.find((value) => value.id === id);
+
+ if (!message) {
+ state.not_found = headers.value.not_found = true;
+ return;
+ }
+ const invokedSaga = message?.invoked_sagas?.[0];
+ state.data.message_id = message.message_id;
+ state.data.conversation_id = message.conversation_id;
+ state.data.body_url = message.body_url;
+ state.data.message_type = message.message_type;
+ state.data.sending_endpoint = message.sending_endpoint;
+ state.data.receiving_endpoint = message.receiving_endpoint;
+ state.data.status = message.status;
+ state.data.processed_at = message.processed_at;
+ if (invokedSaga) {
+ state.data.invoked_saga.has_saga = true;
+ state.data.invoked_saga.saga_id = invokedSaga.saga_id;
+ state.data.invoked_saga.saga_type = invokedSaga.saga_type;
+ }
+ headers.value.data = message.headers;
+ } catch {
+ state.failed_to_load = headers.value.failed_to_load = true;
+ } finally {
+ state.loading = headers.value.loading = false;
+ }
+ }
+
+ async function loadConversation(conversationId: string) {
+ if (conversationId === conversationLoadedId) {
+ return;
+ }
+
+ conversationLoadedId = conversationId;
+ conversationData.value.loading = true;
+ try {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl(`conversations/${conversationId}`);
+
+ conversationData.value.data = data;
+ } catch {
+ conversationData.value.failed_to_load = true;
+ } finally {
+ conversationData.value.loading = false;
+ }
+ }
+
+ async function downloadBody() {
+ if (!state.data.body_url) {
+ return;
+ }
+ if (state.data.id === bodyLoadedId) {
+ return;
+ }
+
+ bodyLoadedId = state.data.id ?? "";
+ body.value.loading = true;
+ body.value.failed_to_load = false;
+
+ try {
+ const response = await serviceControlStore.fetchFromServiceControl(state.data.body_url.substring(1));
+ if (response.status === 404) {
+ body.value.not_found = true;
+
+ return;
+ }
+
+ if (response.status === 204) {
+ body.value.data.no_content = true;
+
+ return;
+ }
+
+ const contentType = response.headers.get("content-type");
+ body.value.data.content_type = contentType ?? "text/plain";
+ body.value.data.value = await response.text();
+
+ if (contentType === "application/json") {
+ body.value.data.value = stringify(parse(body.value.data.value), null, 2) ?? body.value.data.value;
+ }
+ if (contentType === "text/xml") {
+ body.value.data.value = xmlFormat(body.value.data.value, { indentation: " ", collapseContent: true });
+ }
+ } catch {
+ body.value.failed_to_load = true;
+ } finally {
+ body.value.loading = false;
+ }
+ }
+
+ async function archiveMessage() {
+ if (state.data.id) {
+ const response = await serviceControlStore.patchToServiceControl("errors/archive/", [state.data.id]);
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ state.data.failure_status.archiving = true;
+ }
+ }
+
+ async function restoreMessage() {
+ if (state.data.id) {
+ const response = await serviceControlStore.patchToServiceControl("errors/unarchive/", [state.data.id]);
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ state.data.failure_status.restoring = true;
+ }
+ }
+
+ async function retryMessage() {
+ if (state.data.id) {
+ await retryMessages([state.data.id]);
+ state.data.failure_status.retry_in_progress = true;
+ }
+ }
+
+ async function retryMessages(ids: string[]) {
+ const response = await serviceControlStore.postToServiceControl("errors/retry", ids);
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+ }
+
+ async function retryEditedMessage(id: string, editedMessage: Ref) {
+ const payload = {
+ message_body: editedMessage.value.messageBody,
+ message_headers: areSimpleHeadersSupported.value
+ ? editedMessage.value.headers.reduce(
+ (result, header) => {
+ const { key, value } = header as { key: string; value: string };
+ result[key] = value;
+ return result;
+ },
+ {} as { [key: string]: string }
+ )
+ : editedMessage.value.headers,
+ };
+ const response = await serviceControlStore.postToServiceControl(`edit/${id}`, payload);
+ if (!response.ok) {
+ throw new Error(response.statusText);
+ }
+
+ //older versions of SC return no payload about the edit result
+ const bodyText = await response.text();
+ if (bodyText === "") {
+ editRetryResponse.value = {
+ edit_ignored: false,
+ };
+ } else {
+ editRetryResponse.value = parse(bodyText) as EditRetryResponse;
+ }
+ }
+
+ async function pollForNextUpdate(status: FailedMessageStatus) {
+ if (!state.data.id) {
+ return;
+ }
+
+ let maxRetries = 60; // We try for 60 seconds
+
+ do {
+ // eslint-disable-next-line no-await-in-loop
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ // eslint-disable-next-line no-await-in-loop
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl(`errors/last/${state.data.id}`);
+ if (status === data.status) {
+ break;
+ }
+ } while (maxRetries-- > 0);
+
+ if (maxRetries === 0) {
+ // It never changed so no need to refresh UI
+ return;
+ }
+
+ const id = state.data.id;
+ reset();
+ await loadFailedMessage(id);
+ }
+
+ async function exportMessage() {
+ if (state.failed_to_load || state.not_found) {
+ return "";
+ }
+
+ let exportString = "";
+ if (state.data.failure_metadata.exception?.stack_trace !== undefined) {
+ exportString += "STACKTRACE\n";
+ exportString += state.data.failure_metadata.exception.stack_trace;
+ exportString += "\n\n";
+ }
+
+ exportString += "HEADERS";
+ for (let i = 0; i < headers.value.data.length; i++) {
+ exportString += `\n${headers.value.data[i].key}: ${headers.value.data[i].value}`;
+ }
+
+ await downloadBody();
+
+ if (!(body.value.not_found || body.value.failed_to_load || body.value.data.no_content)) {
+ exportString += "\n\nMESSAGE BODY\n";
+ exportString += body.value.data.value;
+ }
+
+ return exportString;
+ }
+
+ return {
+ headers,
+ body,
+ state,
+ edit_and_retry_config,
+ editRetryResponse,
+ reset,
+ loadMessage,
+ loadFailedMessage,
+ loadConversation,
+ downloadBody,
+ exportMessage,
+ archiveMessage,
+ restoreMessage,
+ retryMessage,
+ retryMessages,
+ conversationData,
+ pollForNextUpdate,
+ retryEditedMessage,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useMessageStore, import.meta.hot));
+}
+
+export type MessageStore = ReturnType;
diff --git a/frontend/src/stores/MonitoringEndpointDetailsStore.ts b/frontend/src/stores/MonitoringEndpointDetailsStore.ts
new file mode 100644
index 0000000000..866fb79241
--- /dev/null
+++ b/frontend/src/stores/MonitoringEndpointDetailsStore.ts
@@ -0,0 +1,129 @@
+import { defineStore, acceptHMRUpdate } from "pinia";
+import { ref } from "vue";
+import MessageTypes from "@/components/monitoring/messageTypes";
+import { formatGraphDuration } from "../components/monitoring/formatGraph";
+import { type ExtendedEndpointDetails, type ExtendedEndpointInstance, type MessageType, type EndpointDetails, type EndpointDetailsError, isError } from "@/resources/MonitoringEndpoint";
+import { useMonitoringHistoryPeriodStore } from "./MonitoringHistoryPeriodStore";
+import createMessageGroupClient from "../components/failedmessages/messageGroupClient";
+import type GroupOperation from "@/resources/GroupOperation";
+import { emptyEndpointDetails } from "@/components/monitoring/endpoints";
+import { useMemoize } from "@vueuse/core";
+import useConnectionsAndStatsAutoRefresh from "@/composables/useConnectionsAndStatsAutoRefresh";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export const useMonitoringEndpointDetailsStore = defineStore("MonitoringEndpointDetailsStore", () => {
+ const historyPeriodStore = useMonitoringHistoryPeriodStore();
+ const { store: connectionStore } = useConnectionsAndStatsAutoRefresh();
+ const serviceControlStore = useServiceControlStore();
+ const messageGroupClient = createMessageGroupClient();
+
+ const getMemoisedEndpointDetails = useMemoize((endpointName: string, historyPeriod = 1) => {
+ const data = ref(null);
+ return {
+ data,
+ refresh: async () => {
+ if (serviceControlStore.isMonitoringEnabled) {
+ try {
+ const [, details] = await serviceControlStore.fetchTypedFromMonitoring(`${`monitored-endpoints`}/${endpointName}?history=${historyPeriod}`);
+ data.value = details!;
+ } catch (error) {
+ console.error(error);
+ if (error instanceof Error) {
+ data.value = { error: error.message } as EndpointDetailsError;
+ }
+ }
+ }
+ },
+ };
+ });
+
+ const endpointName = ref("");
+ const endpointDetails = ref(emptyEndpointDetails());
+ const endpointError = ref(null);
+ const messageTypes = ref(null);
+ const messageTypesAvailable = ref(false);
+ const messageTypesUpdatedSet = ref([]);
+ const negativeCriticalTimeIsPresent = ref(false);
+
+ async function getEndpointDetails(name: string) {
+ const { data, refresh } = getMemoisedEndpointDetails(name, historyPeriodStore.historyPeriod.pVal);
+ if (!connectionStore.monitoringConnectionState.unableToConnect) await refresh();
+
+ if (data.value == null || isError(data.value)) {
+ endpointDetails.value.instances.forEach((item) => (item.isScMonitoringDisconnected = true));
+ endpointDetails.value.isScMonitoringDisconnected = true;
+ endpointError.value = data.value;
+ } else {
+ endpointError.value = null;
+ const returnedEndpointDetails = data.value as EndpointDetails;
+ endpointDetails.value.isScMonitoringDisconnected = false;
+
+ const instances = await Promise.all(
+ returnedEndpointDetails.instances.map(async (instance): Promise => {
+ //get error count by instance id
+ const { serviceControlId, errorCount } = await getFailureDetails("Endpoint Instance", instance.id);
+ return { ...instance, serviceControlId, errorCount, isScMonitoringDisconnected: false };
+ })
+ );
+ instances.sort((a, b) => a.id.localeCompare(b.id));
+
+ endpointDetails.value.isStale = instances.every((instance) => instance.isStale);
+
+ if (name === endpointName.value && endpointDetails.value.messageTypes.length > 0 && endpointDetails.value.messageTypes.length !== returnedEndpointDetails.messageTypes.length) {
+ const { messageTypes: returnedMessageTypes, ...dataWithoutMessageTypes } = returnedEndpointDetails;
+ endpointDetails.value = { ...endpointDetails.value, ...dataWithoutMessageTypes, instances };
+
+ messageTypesAvailable.value = true;
+ messageTypesUpdatedSet.value = returnedMessageTypes;
+ } else {
+ endpointDetails.value = { ...endpointDetails.value, ...data.value, instances };
+ }
+
+ endpointName.value = name;
+
+ messageTypes.value = new MessageTypes(endpointDetails.value.messageTypes);
+ negativeCriticalTimeIsPresent.value = endpointDetails.value.instances.some((instance) => parseInt(formatGraphDuration(instance.metrics.criticalTime).value) < 0);
+ }
+
+ //get error count by endpoint name
+ const { serviceControlId, errorCount } = await getFailureDetails("Endpoint Name", endpointName.value);
+ endpointDetails.value.serviceControlId = serviceControlId;
+ endpointDetails.value.errorCount = errorCount;
+ }
+
+ async function getFailureDetails(classifier: string, classifierFilter: string) {
+ const failedMessages: GroupOperation[] = await messageGroupClient.getExceptionGroupsForEndpoint(classifier, classifierFilter);
+ const groupOperation: GroupOperation | undefined = failedMessages[0];
+ return {
+ serviceControlId: groupOperation?.id ?? "",
+ errorCount: groupOperation?.count ?? 0,
+ };
+ }
+
+ function updateMessageTypes() {
+ if (messageTypesAvailable.value) {
+ messageTypesAvailable.value = false;
+ endpointDetails.value.messageTypes = messageTypesUpdatedSet.value;
+ messageTypesUpdatedSet.value = [];
+ messageTypes.value = new MessageTypes(endpointDetails.value.messageTypes);
+ }
+ }
+
+ return {
+ endpointName,
+ endpointDetails,
+ endpointError,
+ messageTypes,
+ messageTypesAvailable,
+ messageTypesUpdatedSet,
+ negativeCriticalTimeIsPresent,
+ updateMessageTypes,
+ getEndpointDetails,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useMonitoringEndpointDetailsStore, import.meta.hot));
+}
+
+export type MonitoringEndpointDetailsStore = ReturnType;
diff --git a/frontend/src/stores/MonitoringHistoryPeriodStore.ts b/frontend/src/stores/MonitoringHistoryPeriodStore.ts
new file mode 100644
index 0000000000..d186961ca6
--- /dev/null
+++ b/frontend/src/stores/MonitoringHistoryPeriodStore.ts
@@ -0,0 +1,58 @@
+import { defineStore, acceptHMRUpdate } from "pinia";
+import { ref } from "vue";
+import { useRoute, useRouter, type RouteLocationNormalizedLoaded } from "vue-router";
+import { useCookies } from "vue3-cookies";
+
+export interface MonitoringHistoryPeriod {
+ pVal: number;
+ text: string;
+ refreshIntervalVal: number;
+ refreshIntervalText: string;
+}
+
+export const useMonitoringHistoryPeriodStore = defineStore("MonitoringHistoryPeriodStore", () => {
+ const { cookies } = useCookies();
+ const route = useRoute();
+ const router = useRouter();
+
+ const periods: MonitoringHistoryPeriod[] = [
+ { pVal: 1, text: "1m", refreshIntervalVal: 1 * 1000, refreshIntervalText: "Show data from the last minute. Refreshes every 1 second" },
+ { pVal: 5, text: "5m", refreshIntervalVal: 5 * 1000, refreshIntervalText: "Show data from the last 5 minutes. Refreshes every 5 seconds" },
+ { pVal: 10, text: "10m", refreshIntervalVal: 10 * 1000, refreshIntervalText: "Show data from the last 10 minutes. Refreshes every 10 seconds" },
+ { pVal: 15, text: "15m", refreshIntervalVal: 15 * 1000, refreshIntervalText: "Show data from the last 15 minutes. Refreshes every 15 seconds" },
+ { pVal: 30, text: "30m", refreshIntervalVal: 30 * 1000, refreshIntervalText: "Show data from the last 30 minutes. Refreshes every 30 seconds" },
+ { pVal: 60, text: "1h", refreshIntervalVal: 60 * 1000, refreshIntervalText: "Show data from the last hour. Refreshes every 1 minute" },
+ ];
+
+ function getHistoryPeriod(route?: RouteLocationNormalizedLoaded, requestedPeriod?: string) {
+ const period = requestedPeriod ?? (route?.query?.historyPeriod?.toString() || cookies.get("history_period"));
+
+ return allPeriods.value.find((index) => index.pVal === parseInt(period)) ?? periods[0];
+ }
+
+ const allPeriods = ref(periods);
+
+ const historyPeriod = ref(getHistoryPeriod(route));
+
+ /**
+ * @param {String} requestedPeriod - The history period value
+ * @description Sets the history period based on, in order of importance, a passed parameter, the url query string, saved cookie, or default value
+ */
+ async function setHistoryPeriod(requestedPeriod?: string) {
+ historyPeriod.value = getHistoryPeriod(route, requestedPeriod);
+ cookies.set("history_period", historyPeriod.value.pVal.toString());
+ await router.replace({ query: { ...route.query, historyPeriod: historyPeriod.value.pVal } });
+ }
+
+ return {
+ allPeriods,
+ historyPeriod,
+ setHistoryPeriod,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useMonitoringHistoryPeriodStore, import.meta.hot));
+}
+
+export type MonitoringHistoryPeriodStore = ReturnType;
diff --git a/frontend/src/stores/MonitoringStore.ts b/frontend/src/stores/MonitoringStore.ts
new file mode 100644
index 0000000000..1890b5bdb8
--- /dev/null
+++ b/frontend/src/stores/MonitoringStore.ts
@@ -0,0 +1,228 @@
+import { defineStore, acceptHMRUpdate } from "pinia";
+import { computed, ref, watch } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { useMonitoringHistoryPeriodStore } from "./MonitoringHistoryPeriodStore";
+import type { EndpointGroup, Endpoint, GroupedEndpoint } from "@/resources/MonitoringEndpoint";
+import type { SortInfo } from "@/components/SortInfo";
+import useConnectionsAndStatsAutoRefresh from "@/composables/useConnectionsAndStatsAutoRefresh";
+import { useServiceControlStore } from "./ServiceControlStore";
+import GroupOperation from "@/resources/GroupOperation";
+
+export const useMonitoringStore = defineStore("MonitoringStore", () => {
+ const historyPeriodStore = useMonitoringHistoryPeriodStore();
+
+ const route = useRoute();
+ const router = useRouter();
+ const { store: connectionStore } = useConnectionsAndStatsAutoRefresh();
+ const serviceControlStore = useServiceControlStore();
+
+ //STORE STATE CONSTANTS
+ const grouping = ref({
+ groupedEndpoints: [] as EndpointGroup[],
+ groupSegments: 0,
+ selectedGrouping: 0,
+ });
+
+ const sortBy = ref({
+ property: "name",
+ isAscending: true,
+ });
+
+ const endpointList = ref([]);
+ const disconnectedEndpointCount = ref(0);
+ const filterString = ref("");
+ const endpointListCount = computed(() => endpointList.value.length);
+ const endpointListIsEmpty = computed(() => endpointListCount.value === 0);
+ const endpointListIsGrouped = computed(() => grouping.value.selectedGrouping !== 0);
+ const getEndpointList = computed(() => (filterString.value ? endpointList.value.filter((endpoint) => endpoint.name.toLowerCase().includes(filterString.value.toLowerCase())) : endpointList.value));
+
+ watch(sortBy, async () => await updateEndpointList(), { deep: true });
+ watch(filterString, async (newValue) => {
+ await updateFilterString(newValue);
+ });
+
+ //STORE ACTIONS
+ async function updateFilterString(filter: string | null = null) {
+ filterString.value = filter ?? route.query.filter?.toString() ?? "";
+
+ if (filterString.value === "") {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { filter, ...withoutFilter } = route.query;
+ await router.replace({ query: withoutFilter }); // Update or add filter query parameter to url
+ } else {
+ await router.replace({ query: { ...route.query, filter: filterString.value } }); // Update or add filter query parameter to url
+ }
+ updateGroupedEndpoints();
+ }
+
+ async function updateEndpointList() {
+ if (connectionStore.monitoringConnectionState.unableToConnect) {
+ endpointList.value = [];
+ } else {
+ endpointList.value = await getAllMonitoredEndpoints();
+ }
+ if (!endpointListIsEmpty.value) {
+ updateGroupSegments();
+ if (endpointListIsGrouped.value) {
+ updateGroupedEndpoints();
+ } else {
+ sortEndpointList();
+ }
+ }
+ }
+
+ async function getAllMonitoredEndpoints() {
+ let endpoints: Endpoint[] = [];
+ if (serviceControlStore.isMonitoringEnabled) {
+ try {
+ const [, data] = await serviceControlStore.fetchTypedFromMonitoring(`monitored-endpoints?history=${historyPeriodStore.historyPeriod.pVal}`);
+ endpoints = data ?? [];
+ const [, exceptionGroups] = await serviceControlStore.fetchTypedFromServiceControl(`recoverability/groups/Endpoint Name`);
+
+ //Squash and add to existing monitored endpoints
+ if (exceptionGroups.length > 0) {
+ //sort the exceptionGroups array by name - case sensitive
+ exceptionGroups.sort((a, b) => (a.title > b.title ? 1 : a.title < b.title ? -1 : 0)); //desc
+ exceptionGroups
+ .filter((exceptionGroup) => exceptionGroup.operation_status !== "ArchiveCompleted")
+ .forEach((exceptionGroup) => {
+ const monitoredEndpoint = endpoints.find((item) => item.name === exceptionGroup.title);
+ if (monitoredEndpoint) {
+ monitoredEndpoint.serviceControlId = exceptionGroup.id;
+ monitoredEndpoint.errorCount = exceptionGroup.count;
+ }
+ });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ return endpoints;
+ }
+
+ function updateSelectedGrouping(groupSize: number) {
+ grouping.value.selectedGrouping = groupSize;
+ if (groupSize === 0) {
+ sortEndpointList();
+ } else {
+ updateGroupedEndpoints();
+ }
+ }
+
+ function updateGroupSegments() {
+ grouping.value.groupSegments = endpointList.value.reduce((acc, cur) => Math.max(acc, cur.name.split(".").length - 1), 0);
+ }
+
+ function updateGroupedEndpoints() {
+ const groups = new Map();
+ for (const element of getEndpointList.value) {
+ const newGrouping = parseEndpoint(element, grouping.value.selectedGrouping);
+
+ const resultGroup = groups.get(newGrouping.groupName) ?? {
+ group: newGrouping.groupName,
+ endpoints: [],
+ };
+ resultGroup.endpoints.push(newGrouping);
+ groups.set(newGrouping.groupName, resultGroup);
+ }
+
+ grouping.value.groupedEndpoints = [...groups.values()];
+ sortGroupedEndpointList();
+ }
+
+ function parseEndpoint(endpoint: Endpoint, maxGroupSegments: number) {
+ if (maxGroupSegments === 0) {
+ return {
+ groupName: "Ungrouped",
+ shortName: endpoint.name,
+ endpoint: endpoint,
+ };
+ }
+
+ const segments = endpoint.name.split(".");
+ const groupSegments = segments.slice(0, maxGroupSegments);
+ const endpointSegments = segments.slice(maxGroupSegments);
+ if (endpointSegments.length === 0) {
+ // the endpoint's name is shorter than the group size
+ return parseEndpoint(endpoint, maxGroupSegments - 1);
+ }
+
+ return {
+ groupName: groupSegments.join("."),
+ shortName: endpointSegments.join("."),
+ endpoint,
+ } as GroupedEndpoint;
+ }
+
+ function sortEndpointList() {
+ const comparator = (() => {
+ if (sortBy.value.property === "name") {
+ return (a: Endpoint, b: Endpoint) => (sortBy.value.isAscending ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name));
+ } else {
+ return (a: Endpoint, b: Endpoint) => {
+ const propertyA = a.metrics[sortBy.value.property].average;
+ const propertyB = b.metrics[sortBy.value.property].average;
+
+ return sortBy.value.isAscending ? propertyA - propertyB : propertyB - propertyA;
+ };
+ }
+ })();
+
+ endpointList.value.sort(comparator);
+ }
+
+ function sortGroupedEndpointList() {
+ let comparator;
+ const endpointShortNameComparator = (a: GroupedEndpoint, b: GroupedEndpoint) => {
+ return sortBy.value.isAscending ? a.shortName.localeCompare(b.shortName) : b.shortName.localeCompare(a.shortName);
+ };
+
+ if (sortBy.value.property === "name") {
+ comparator = (a: EndpointGroup, b: EndpointGroup) => {
+ const groupNameA = a.group;
+ const groupNameB = b.group;
+ const endpointListGroupA = a.endpoints;
+ const endpointListGroupB = b.endpoints;
+
+ // Sort each group's endpoints before sorting the group name
+ endpointListGroupA.sort(endpointShortNameComparator);
+ endpointListGroupB.sort(endpointShortNameComparator);
+
+ return sortBy.value.isAscending ? groupNameA.localeCompare(groupNameB) : groupNameB.localeCompare(groupNameA);
+ };
+ }
+ // TODO: Determine how sorting should be handled for columns other than endpoint name
+
+ if (grouping.value.groupedEndpoints.length > 1) {
+ grouping.value.groupedEndpoints.sort(comparator);
+ } else if (grouping.value.groupedEndpoints.length === 1) {
+ grouping.value.groupedEndpoints[0].endpoints.sort(endpointShortNameComparator);
+ }
+ }
+
+ return {
+ //state
+ grouping,
+ endpointList,
+ disconnectedEndpointCount,
+ filterString,
+ sortBy,
+
+ //getters
+ endpointListCount,
+ endpointListIsEmpty,
+ endpointListIsGrouped,
+ getEndpointList,
+
+ //actions
+ updateSelectedGrouping,
+ updateEndpointList,
+ updateFilterString,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useMonitoringStore, import.meta.hot));
+}
+
+export type MonitoringStore = ReturnType;
diff --git a/frontend/src/stores/RedirectsStore.ts b/frontend/src/stores/RedirectsStore.ts
new file mode 100644
index 0000000000..a27578e090
--- /dev/null
+++ b/frontend/src/stores/RedirectsStore.ts
@@ -0,0 +1,53 @@
+import Redirect from "@/resources/Redirect";
+import QueueAddress from "@/resources/QueueAddress";
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { reactive } from "vue";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export interface Redirects {
+ data: Redirect[];
+ queues: string[];
+ total: number;
+}
+
+export const useRedirectsStore = defineStore("RedirectsStore", () => {
+ const redirects = reactive({
+ data: [],
+ queues: [],
+ total: 0,
+ });
+
+ const serviceControlStore = useServiceControlStore();
+
+ async function getKnownQueues() {
+ const [, data] = await serviceControlStore.fetchTypedFromServiceControl("errors/queues/addresses");
+ redirects.queues = data.map((x) => x.physical_address);
+ }
+
+ async function getRedirects() {
+ const [response, data] = await serviceControlStore.fetchTypedFromServiceControl("redirects");
+ redirects.total = parseInt(response.headers.get("Total-Count") || "0");
+ redirects.data = data;
+ }
+
+ async function refresh() {
+ await Promise.all([getRedirects(), getKnownQueues()]);
+ }
+
+ async function retryPendingMessagesForQueue(queueName: string) {
+ const response = await serviceControlStore.postToServiceControl(`errors/queues/${queueName}/retry`);
+ return {
+ message: response.ok ? "success" : `error:${response.statusText}`,
+ status: response.status,
+ statusText: response.statusText,
+ };
+ }
+
+ return { refresh, redirects, retryPendingMessagesForQueue };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useRedirectsStore, import.meta.hot));
+}
+
+export type RedirectsStore = ReturnType;
diff --git a/frontend/src/stores/SagaDiagramStore.ts b/frontend/src/stores/SagaDiagramStore.ts
new file mode 100644
index 0000000000..4a14a6e15e
--- /dev/null
+++ b/frontend/src/stores/SagaDiagramStore.ts
@@ -0,0 +1,251 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { ref, watch } from "vue";
+import { SagaHistory, SagaMessage } from "@/resources/SagaHistory";
+import Message from "@/resources/Message";
+import { parse, stringify } from "lossless-json";
+import xmlFormat from "xml-formatter";
+import { DataContainer } from "./DataContainer";
+import { useMessageStore } from "./MessageStore";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export interface SagaMessageData {
+ message_id: string;
+ body: DataContainer<{ value?: string; content_type?: string; no_content?: boolean }>;
+}
+export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
+ const sagaHistory = ref(null);
+ const sagaId = ref(null);
+ const loading = ref(false);
+ const messageDataLoading = ref(false);
+ const error = ref(null);
+ const showMessageData = ref(false);
+ const fetchedMessages = ref(new Set());
+ const messagesData = ref([]);
+ const selectedMessageId = ref(null);
+ const scrollToTimeoutRequest = ref(false);
+ const scrollToTimeout = ref(false);
+ const MessageBodyEndpoint = "messages/{0}/body";
+ const messageStore = useMessageStore();
+ const serviceControlStore = useServiceControlStore();
+
+ watch(
+ () => messageStore.state.data.message_id,
+ (newMessageId) => {
+ if (newMessageId) {
+ setSelectedMessageId(newMessageId);
+ }
+ },
+ { immediate: true }
+ );
+
+ // Watch the sagaId and fetch saga history when it changes
+ watch(sagaId, async (newSagaId) => {
+ if (newSagaId) {
+ await fetchSagaHistory(newSagaId);
+ } else {
+ clearSagaHistory();
+ }
+ });
+
+ // Watch both showMessageData and sagaHistory together
+ watch([showMessageData, sagaHistory], async ([show, history]) => {
+ if (show && history) {
+ await fetchMessagesData(history);
+ }
+ });
+
+ function setSagaId(id: string | null) {
+ sagaId.value = id;
+ }
+
+ async function fetchSagaHistory(id: string) {
+ if (!id) return;
+
+ loading.value = true;
+ error.value = null;
+
+ try {
+ const response = await serviceControlStore.fetchFromServiceControl(`sagas/${id}`);
+
+ if (response.status === 404) {
+ sagaHistory.value = null;
+ error.value = "Saga history not found";
+ } else if (!response.ok) {
+ sagaHistory.value = null;
+ error.value = "Failed to fetch saga history";
+ } else {
+ const data = await response.json();
+ sagaHistory.value = data;
+ }
+ } catch (e) {
+ error.value = e instanceof Error ? e.message : "Unknown error occurred";
+ sagaHistory.value = null;
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ async function fetchSagaMessageData(message: SagaMessage): Promise {
+ const bodyUrl = (message.body_url ?? formatUrl(MessageBodyEndpoint, message.message_id)).replace(/^\//, "");
+ const result: SagaMessageData = {
+ message_id: message.message_id,
+ body: { data: {} },
+ };
+
+ result.body.loading = true;
+ result.body.failed_to_load = false;
+
+ try {
+ const response = await serviceControlStore.fetchFromServiceControl(bodyUrl);
+ if (response.status === 404) {
+ result.body.not_found = true;
+ return result;
+ }
+
+ if (response.status === 204) {
+ result.body.data.no_content = true;
+ return result;
+ }
+
+ const contentType = response.headers.get("content-type");
+ result.body.data.content_type = contentType ?? "text/plain";
+ result.body.data.value = await response.text();
+
+ if (contentType === "application/json" && result.body.data.value) {
+ // Only format non-empty JSON objects
+ result.body.data.value = result.body.data.value !== "{}" ? (stringify(parse(result.body.data.value), null, 2) ?? result.body.data.value) : "";
+ } else if (contentType === "text/xml" && result.body.data.value) {
+ // Format XML if it has content in the root element
+ const xmlRootElement = getContentOfXmlRootElement(result.body.data.value);
+ result.body.data.value = xmlRootElement ? xmlFormat(result.body.data.value, { indentation: " ", collapseContent: true }) : "";
+ }
+ } catch {
+ result.body.failed_to_load = true;
+ } finally {
+ result.body.loading = false;
+ }
+
+ return result;
+ }
+
+ function getContentOfXmlRootElement(xml: string): string {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(xml, "text/xml");
+ const rootElement = doc.documentElement;
+ if (rootElement) {
+ const rootElementText = rootElement.textContent;
+ if (rootElementText) {
+ return rootElementText;
+ }
+ }
+ return "";
+ }
+
+ async function getAuditMessages(sagaId: string) {
+ try {
+ const response = await serviceControlStore.fetchFromServiceControl(`messages/search?q=${sagaId}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return await response.json();
+ } catch (error) {
+ console.error("Error fetching audit messages:", error);
+ return { result: [] };
+ }
+ }
+
+ function clearSagaHistory() {
+ sagaHistory.value = null;
+ sagaId.value = null;
+ error.value = null;
+ fetchedMessages.value.clear();
+ messagesData.value = [];
+ selectedMessageId.value = null;
+ scrollToTimeoutRequest.value = false;
+ }
+
+ function formatUrl(template: string, id: string): string {
+ return template.replace("{0}", id);
+ }
+
+ function toggleMessageData() {
+ showMessageData.value = !showMessageData.value;
+ }
+
+ async function fetchMessagesData(history: SagaHistory) {
+ messageDataLoading.value = true;
+ error.value = null;
+
+ try {
+ // Get all messages from changes array - both initiating and outgoing
+ const messagesToFetch = history.changes.flatMap((change) => {
+ const messages: SagaMessage[] = [];
+
+ // Add initiating message if it exists and hasn't been fetched
+ if (change.initiating_message && !fetchedMessages.value.has(change.initiating_message.message_id)) {
+ messages.push(change.initiating_message);
+ }
+
+ // Add all unfetched outgoing messages
+ if (change.outgoing_messages) {
+ messages.push(...change.outgoing_messages.filter((msg) => !fetchedMessages.value.has(msg.message_id)));
+ }
+ return messages;
+ });
+
+ // Check if any messages need body_url
+ const needsBodyUrl = messagesToFetch.every((msg) => !msg.body_url);
+ if (needsBodyUrl && messagesToFetch.length > 0) {
+ const auditMessages = await getAuditMessages(sagaId.value!);
+ messagesToFetch.forEach((message) => {
+ const auditMessage = auditMessages.find((x: Message) => x.message_id === message.message_id);
+ if (auditMessage) {
+ message.body_url = auditMessage.body_url;
+ }
+ });
+ }
+
+ // Fetch data for each unfetched message in parallel and store results
+ const fetchPromises = messagesToFetch.map(async (message) => {
+ const data = await fetchSagaMessageData(message);
+ fetchedMessages.value.add(message.message_id);
+ return data;
+ });
+
+ const newMessageData = await Promise.all(fetchPromises);
+ // Add new message data to the existing array
+ messagesData.value = [...messagesData.value, ...newMessageData];
+ } catch (e) {
+ error.value = e instanceof Error ? e.message : "Unknown error occurred";
+ } finally {
+ messageDataLoading.value = false;
+ }
+ }
+
+ function setSelectedMessageId(messageId: string | null) {
+ selectedMessageId.value = messageId;
+ }
+
+ return {
+ sagaHistory,
+ sagaId,
+ loading,
+ messageDataLoading,
+ error,
+ showMessageData,
+ messagesData,
+ selectedMessageId,
+ scrollToTimeoutRequest,
+ scrollToTimeout,
+ setSagaId,
+ clearSagaHistory,
+ toggleMessageData,
+ setSelectedMessageId,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useSagaDiagramStore, import.meta.hot));
+}
+
+export type SagaDiagramStore = ReturnType;
diff --git a/frontend/src/stores/SequenceDiagramStore.ts b/frontend/src/stores/SequenceDiagramStore.ts
new file mode 100644
index 0000000000..56dee8f863
--- /dev/null
+++ b/frontend/src/stores/SequenceDiagramStore.ts
@@ -0,0 +1,124 @@
+import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia";
+import { computed, ref, watch } from "vue";
+import { ModelCreator } from "@/resources/SequenceDiagram/SequenceModel";
+import { Endpoint } from "@/resources/SequenceDiagram/Endpoint";
+import { Handler } from "@/resources/SequenceDiagram/Handler";
+import { MessageProcessingRoute } from "@/resources/SequenceDiagram/RoutedMessage";
+import { useMessageStore } from "./MessageStore";
+import { useRouter } from "vue-router";
+import routeLinks from "@/router/routeLinks";
+
+export interface EndpointCentrePoint {
+ name: string;
+ centre?: number;
+ top: number;
+}
+
+export interface HandlerLocation {
+ id: string;
+ endpointName: string;
+ left: number;
+ right: number;
+ y: number;
+ height: number;
+}
+
+export const Endpoint_Width = 260;
+
+export const useSequenceDiagramStore = defineStore("SequenceDiagramStore", () => {
+ const messageStore = useMessageStore();
+ const { state, conversationData } = storeToRefs(messageStore);
+ const router = useRouter();
+
+ const startX = ref(Endpoint_Width / 2);
+ const endpoints = ref([]);
+ const handlers = ref([]);
+ const routes = ref([]);
+ const endpointCentrePoints = ref([]);
+ const maxWidth = ref(150);
+ const maxHeight = ref(150);
+ const handlerLocations = ref([]);
+ const highlightId = ref();
+
+ const isLoading = computed(() => conversationData.value.loading);
+ const selectedId = computed(() => `${state.value.data.message_type ?? ""}(${state.value.data.id})`);
+
+ watch(
+ () => conversationData.value.data,
+ (conversationData) => {
+ if (conversationData.length) {
+ startX.value = Endpoint_Width / 2;
+ const model = new ModelCreator(conversationData);
+ endpoints.value = model.endpoints;
+ handlers.value = model.handlers;
+ routes.value = model.routes;
+ }
+ },
+ { immediate: true }
+ );
+
+ function setStartX(offset: number) {
+ const newValue = Math.max(offset + Endpoint_Width / 2, startX.value);
+ if (newValue === startX.value) return;
+ startX.value = newValue;
+ }
+
+ function setMaxWidth(width: number) {
+ maxWidth.value = width;
+ }
+
+ function setMaxHeight(height: number) {
+ maxHeight.value = height;
+ }
+
+ function setEndpointCentrePoints(centrePoints: EndpointCentrePoint[]) {
+ endpointCentrePoints.value = centrePoints;
+ }
+
+ function setHandlerLocations(locations: HandlerLocation[]) {
+ handlerLocations.value = locations;
+ }
+
+ function setHighlightId(id?: string) {
+ highlightId.value = id;
+ }
+
+ function refreshConversation() {
+ if (messageStore.state.data.conversation_id) messageStore.loadConversation(messageStore.state.data.conversation_id);
+ }
+
+ function navigateTo(messageUniqueId: string | undefined, messageId: string | undefined, isError: boolean) {
+ if (messageUniqueId == null) return;
+ if (!isError && messageId == null) return;
+
+ router.push({ path: isError ? routeLinks.messages.failedMessage.link(messageUniqueId) : routeLinks.messages.successMessage.link(messageId!, messageUniqueId) });
+ }
+
+ return {
+ startX,
+ endpoints,
+ handlers,
+ routes,
+ endpointCentrePoints,
+ maxWidth,
+ maxHeight,
+ handlerLocations,
+ highlightId,
+ selectedId,
+ isLoading,
+ setStartX,
+ setMaxWidth,
+ setMaxHeight,
+ setEndpointCentrePoints,
+ setHandlerLocations,
+ setHighlightId,
+ refreshConversation,
+ navigateTo,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useSequenceDiagramStore, import.meta.hot));
+}
+
+export type SequenceDiagramStore = ReturnType;
diff --git a/frontend/src/stores/ServiceControlStore.ts b/frontend/src/stores/ServiceControlStore.ts
new file mode 100644
index 0000000000..aeb38a855e
--- /dev/null
+++ b/frontend/src/stores/ServiceControlStore.ts
@@ -0,0 +1,164 @@
+import { acceptHMRUpdate, defineStore } from "pinia";
+import { computed, ref } from "vue";
+import { getDefaultConfig } from "@/defaultConfig";
+
+export const useServiceControlStore = defineStore("ServiceControlStore", () => {
+ const serviceControlUrl = ref();
+ const monitoringUrl = ref();
+
+ const isMonitoringDisabled = computed(() => monitoringUrl.value == null || monitoringUrl.value === "" || monitoringUrl.value === "!");
+ const isMonitoringEnabled = computed(() => !isMonitoringDisabled.value);
+
+ function getServiceControlUrl() {
+ if (!serviceControlUrl.value) {
+ refresh();
+ }
+ if (!serviceControlUrl.value) {
+ throw new Error("Service Control URL is not configured");
+ }
+ return serviceControlUrl.value;
+ }
+
+ function getMonitoringUrl() {
+ if (!monitoringUrl.value) refresh();
+ return monitoringUrl.value;
+ }
+
+ function refresh() {
+ const params = new URLSearchParams(window.location.search);
+ const mu = params.get("mu");
+ const config = getDefaultConfig();
+
+ if (config.service_control_url) {
+ serviceControlUrl.value = config.service_control_url;
+ console.debug(`setting ServiceControl Url to its default value: ${config.service_control_url}`);
+ } else {
+ console.warn("ServiceControl Url is not defined.");
+ }
+
+ if (mu) {
+ monitoringUrl.value = mu;
+ window.localStorage.setItem("mu", monitoringUrl.value);
+ console.debug(`Monitoring Url found in QS and stored in local storage: ${monitoringUrl.value}`);
+ } else if (window.localStorage.getItem("mu")) {
+ monitoringUrl.value = window.localStorage.getItem("mu");
+ console.debug(`Monitoring Url, not in QS, found in local storage: ${monitoringUrl.value}`);
+ } else if (config.monitoring_url) {
+ monitoringUrl.value = config.monitoring_url;
+ console.debug(`setting Monitoring Url to its default value: ${config.monitoring_url}`);
+ } else {
+ console.warn("Monitoring Url is not defined.");
+ }
+ }
+
+ async function fetchFromServiceControl(suffix: string, options?: { cache?: RequestCache }) {
+ const requestOptions: RequestInit = {
+ method: "GET",
+ cache: options?.cache ?? "default", // Default if not specified
+ headers: {
+ Accept: "application/json",
+ },
+ };
+ return await fetch(`${getServiceControlUrl()}${suffix}`, requestOptions);
+ }
+
+ async function fetchTypedFromServiceControl(suffix: string): Promise<[Response, T]> {
+ const response = await fetch(`${getServiceControlUrl()}${suffix}`);
+ if (!response.ok) throw new Error(response.statusText ?? "No response");
+ const data = await response.json();
+
+ return [response, data];
+ }
+
+ async function fetchTypedFromMonitoring(suffix: string): Promise<[Response?, T?]> {
+ if (isMonitoringDisabled.value) {
+ return [];
+ }
+
+ const response = await fetch(`${getMonitoringUrl()}${suffix}`);
+ const data = await response.json();
+
+ return [response, data];
+ }
+
+ async function postToServiceControl(suffix: string, payload: object | null = null) {
+ const requestOptions: RequestInit = {
+ method: "POST",
+ };
+ if (payload != null) {
+ requestOptions.headers = { "Content-Type": "application/json" };
+ requestOptions.body = JSON.stringify(payload);
+ }
+ return await fetch(`${getServiceControlUrl()}${suffix}`, requestOptions);
+ }
+
+ async function putToServiceControl(suffix: string, payload: object | null) {
+ const requestOptions: RequestInit = {
+ method: "PUT",
+ };
+ if (payload != null) {
+ requestOptions.headers = { "Content-Type": "application/json" };
+ requestOptions.body = JSON.stringify(payload);
+ }
+ return await fetch(`${getServiceControlUrl()}${suffix}`, requestOptions);
+ }
+
+ async function deleteFromServiceControl(suffix: string) {
+ const requestOptions: RequestInit = {
+ method: "DELETE",
+ };
+ return await fetch(`${getServiceControlUrl()}${suffix}`, requestOptions);
+ }
+
+ async function deleteFromMonitoring(suffix: string) {
+ const requestOptions = {
+ method: "DELETE",
+ };
+ return await fetch(`${getMonitoringUrl()}${suffix}`, requestOptions);
+ }
+
+ async function optionsFromMonitoring() {
+ if (isMonitoringDisabled.value) {
+ return Promise.resolve(null);
+ }
+
+ const requestOptions = {
+ method: "OPTIONS",
+ };
+ return await fetch(getMonitoringUrl() ?? "", requestOptions);
+ }
+
+ async function patchToServiceControl(suffix: string, payload: object | null) {
+ const requestOptions: RequestInit = {
+ method: "PATCH",
+ };
+ if (payload != null) {
+ requestOptions.headers = { "Content-Type": "application/json" };
+ requestOptions.body = JSON.stringify(payload);
+ }
+ return await fetch(`${getServiceControlUrl()}${suffix}`, requestOptions);
+ }
+
+ return {
+ refresh,
+ serviceControlUrl,
+ monitoringUrl,
+ isMonitoringDisabled,
+ isMonitoringEnabled,
+ fetchFromServiceControl,
+ fetchTypedFromServiceControl,
+ fetchTypedFromMonitoring,
+ putToServiceControl,
+ postToServiceControl,
+ patchToServiceControl,
+ deleteFromServiceControl,
+ deleteFromMonitoring,
+ optionsFromMonitoring,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useServiceControlStore, import.meta.hot));
+}
+
+export type ServiceControlStore = ReturnType;
diff --git a/frontend/src/stores/ThroughputStore.spec.ts b/frontend/src/stores/ThroughputStore.spec.ts
new file mode 100644
index 0000000000..f35ca4827b
--- /dev/null
+++ b/frontend/src/stores/ThroughputStore.spec.ts
@@ -0,0 +1,111 @@
+import { beforeEach, describe, expect, test } from "vitest";
+import * as precondition from "../../test/preconditions";
+import { Transport } from "@/views/throughputreport/transport";
+import { makeDriverForTests } from "@component-test-utils";
+import { serviceControlWithThroughput } from "@/views/throughputreport/serviceControlWithThroughput";
+import { useThroughputStore } from "@/stores/ThroughputStore";
+import { createTestingPinia } from "@pinia/testing";
+import { setActivePinia, storeToRefs } from "pinia";
+import { Driver } from "../../test/driver";
+import { disableMonitoring } from "../../test/drivers/vitest/setup";
+import { useEnvironmentAndVersionsStore } from "./EnvironmentAndVersionsStore";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+describe("ThroughputStore tests", () => {
+ async function setup(preSetup: (driver: Driver) => Promise) {
+ const driver = makeDriverForTests();
+ setActivePinia(createTestingPinia({ stubActions: false }));
+
+ await preSetup(driver);
+ await driver.setUp(serviceControlWithThroughput);
+ await driver.setUp(precondition.hasNoDisconnectedEndpoints);
+ await driver.setUp(precondition.hasServiceControlMonitoringInstance);
+
+ useServiceControlStore();
+ await useEnvironmentAndVersionsStore().refresh();
+
+ const store = useThroughputStore();
+ const refs = storeToRefs(store);
+ await store.refresh();
+
+ return { driver, ...refs };
+ }
+
+ test("when no connection test errors for any source", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport: Transport.AmazonSQS }));
+ });
+
+ expect(hasErrors.value).toBe(false);
+ });
+
+ describe("when transport is a broker", () => {
+ const transport = Transport.AmazonSQS;
+
+ test("with broker connection test failure", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, broker_connection_result: { connection_successful: false, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(true);
+ });
+
+ test("with monitoring connection test failure", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, monitoring_connection_result: { connection_successful: false, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(true);
+ });
+
+ test("with audit connection test failure", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, audit_connection_result: { connection_successful: false, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(true);
+ });
+ });
+
+ describe("when transport is not a broker", () => {
+ const transport = Transport.MSMQ;
+
+ test("with monitoring connection test failure", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, monitoring_connection_result: { connection_successful: false, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(true);
+ });
+
+ test("with audit connection test failure", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, audit_connection_result: { connection_successful: false, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(true);
+ });
+
+ describe("with monitoring disabled", () => {
+ beforeEach(() => {
+ disableMonitoring();
+ });
+
+ test("with audit connection test failure", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, audit_connection_result: { connection_successful: false, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(true);
+ });
+
+ test("with audit connection test passing", async () => {
+ const { hasErrors } = await setup(async (driver) => {
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport, audit_connection_result: { connection_successful: true, connection_error_messages: [], diagnostics: "" } }));
+ });
+
+ expect(hasErrors.value).toBe(false);
+ });
+ });
+ });
+});
diff --git a/frontend/src/stores/ThroughputStore.ts b/frontend/src/stores/ThroughputStore.ts
new file mode 100644
index 0000000000..08b133db56
--- /dev/null
+++ b/frontend/src/stores/ThroughputStore.ts
@@ -0,0 +1,122 @@
+import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia";
+import { computed, ref, watch } from "vue";
+import ConnectionTestResults from "@/resources/ConnectionTestResults";
+import createThroughputClient from "@/views/throughputreport/throughputClient";
+import { Transport } from "@/views/throughputreport/transport";
+import useIsThroughputSupported from "@/views/throughputreport/isThroughputSupported";
+import { useServiceControlStore } from "./ServiceControlStore";
+
+export const useThroughputStore = defineStore("ThroughputStore", () => {
+ const serviceControlStore = useServiceControlStore();
+ const { isMonitoringEnabled } = storeToRefs(serviceControlStore);
+
+ const testResults = ref(null);
+ const isThroughputSupported = useIsThroughputSupported();
+ const throughputClient = createThroughputClient();
+
+ const refresh = async () => {
+ if (isThroughputSupported.value) {
+ testResults.value = await throughputClient.test();
+ }
+ };
+
+ const hasErrors = computed(() => {
+ // if it is a broker transport, we return true if connection test is unsuccessful
+ if (isBrokerTransport.value && !testResults.value?.broker_connection_result.connection_successful) {
+ return true;
+ }
+
+ // if Audit connection test fails, we will return true.
+ // the connection test will return true if there are no Audit instances configured.
+ if (!testResults.value?.audit_connection_result.connection_successful) {
+ //TODO: should this be a warning rather than an error?
+ return true;
+ }
+
+ // if Monitoring is enabled, we return whatever the value of the connection test
+ if (isMonitoringEnabled.value) {
+ return !testResults.value?.monitoring_connection_result.connection_successful;
+ }
+
+ // Last but not least we assume no errors
+ return false;
+ });
+ const transport = computed(() => {
+ if (testResults.value == null) {
+ return Transport.None;
+ }
+
+ return testResults.value.transport as Transport;
+ });
+ const isBrokerTransport = computed(() => {
+ switch (transport.value) {
+ case Transport.None:
+ case Transport.MSMQ:
+ case Transport.AzureStorageQueue:
+ case Transport.LearningTransport:
+ return false;
+ default:
+ return true;
+ }
+ });
+ const transportNameForInstructions = () => {
+ switch (transport.value) {
+ case Transport.AzureStorageQueue:
+ return "Azure Storage Queue";
+ case Transport.NetStandardAzureServiceBus:
+ return "Azure Service Bus";
+ case Transport.MSMQ:
+ return "MSMQ";
+ case Transport.LearningTransport:
+ return "Learning Transport";
+ case Transport.RabbitMQ:
+ return "RabbitMQ";
+ case Transport.SQLServer:
+ return "Sql Server";
+ case Transport.AmazonSQS:
+ return "Amazon SQS";
+ case Transport.PostgreSQL:
+ return "PostgreSQL";
+ }
+ };
+ const transportDocsLinkForInstructions = () => {
+ switch (transport.value) {
+ case Transport.AzureStorageQueue:
+ case Transport.LearningTransport:
+ case Transport.MSMQ:
+ return "https://docs.particular.net/servicepulse/usage-config#connection-setup-msmq-azure-storage-queues";
+ case Transport.NetStandardAzureServiceBus:
+ return "https://docs.particular.net/servicepulse/usage-config#connection-setup-azure-service-bus";
+ case Transport.RabbitMQ:
+ return "https://docs.particular.net/servicepulse/usage-config#connection-setup-rabbitmq";
+ case Transport.SQLServer:
+ return "https://docs.particular.net/servicepulse/usage-config#connection-setup-sqlserver";
+ case Transport.AmazonSQS:
+ return "https://docs.particular.net/servicepulse/usage-config#connection-setup-amazon-sqs";
+ case Transport.PostgreSQL:
+ return "https://docs.particular.net/servicepulse/usage-config#connection-setup-postgresql";
+ }
+ };
+
+ watch(isThroughputSupported, (value) => {
+ if (value) {
+ refresh();
+ }
+ });
+
+ return {
+ testResults,
+ refresh,
+ transportNameForInstructions,
+ transportDocsLinkForInstructions,
+ isBrokerTransport,
+ hasErrors,
+ transport,
+ };
+});
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useThroughputStore, import.meta.hot));
+}
+
+export type ThroughputStore = ReturnType;
diff --git a/frontend/src/types/date.ts b/frontend/src/types/date.ts
new file mode 100644
index 0000000000..ab548a3c47
--- /dev/null
+++ b/frontend/src/types/date.ts
@@ -0,0 +1 @@
+export type DateRange = [fromDate: Date, toDate: Date] | [];
diff --git a/frontend/src/views/AuditView.vue b/frontend/src/views/AuditView.vue
new file mode 100644
index 0000000000..2f31ff370a
--- /dev/null
+++ b/frontend/src/views/AuditView.vue
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+ The minimum version of ServiceControl required to enable this feature is
+ {{ minimumSCVersionForAllMessages }} .
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/ConfigurationView.vue b/frontend/src/views/ConfigurationView.vue
new file mode 100644
index 0000000000..ecb4b8e438
--- /dev/null
+++ b/frontend/src/views/ConfigurationView.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+ License
+
+
+
+ Usage Setup
+
+
+
+
+ MassTransit Connector
+
+
+ Health Check Notifications
+
+
+ Retry Redirects ({{ redirectsStore.redirects.total }})
+
+
+
+ Connections
+
+
+
+
+ Endpoint Connection
+
+
+
+
+
+ Connections
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/CustomChecksView.vue b/frontend/src/views/CustomChecksView.vue
new file mode 100644
index 0000000000..17b7e6992e
--- /dev/null
+++ b/frontend/src/views/CustomChecksView.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue
new file mode 100644
index 0000000000..935e89fbaf
--- /dev/null
+++ b/frontend/src/views/DashboardView.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
diff --git a/frontend/src/views/EventsView.vue b/frontend/src/views/EventsView.vue
new file mode 100644
index 0000000000..4d50c64e89
--- /dev/null
+++ b/frontend/src/views/EventsView.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/FailedMessagesView.vue b/frontend/src/views/FailedMessagesView.vue
new file mode 100644
index 0000000000..646ad1b599
--- /dev/null
+++ b/frontend/src/views/FailedMessagesView.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Failed Message Groups
+ (0)
+
+ !
+
+
+
+
+ All Failed Messages
+ {{ failedMessageCount }}
+
+
+
+
+ Deleted Message Groups
+ !
+
+
+
+
+ All Deleted Messages
+ {{ archivedMessageCount }}
+
+
+
+
+ Pending Retries
+ {{ pendingRetriesMessageCount }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/HeartbeatsView.vue b/frontend/src/views/HeartbeatsView.vue
new file mode 100644
index 0000000000..011cb89c11
--- /dev/null
+++ b/frontend/src/views/HeartbeatsView.vue
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
Endpoint Heartbeats
+
+
+
MassTransit endpoints are currently not supported by heartbeat functionality and will not show in this view.
+
+
+
+
+
+
+
+
+ Unhealthy Endpoints ({{ unhealthyEndpoints.length }})
+
+
+
+
+ Healthy Endpoints ({{ healthyEndpoints.length }})
+
+
+
+
+ Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/MonitoringView.vue b/frontend/src/views/MonitoringView.vue
new file mode 100644
index 0000000000..c9ece10aa0
--- /dev/null
+++ b/frontend/src/views/MonitoringView.vue
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/ThroughputReportView.spec.ts b/frontend/src/views/ThroughputReportView.spec.ts
new file mode 100644
index 0000000000..7dd92230b4
--- /dev/null
+++ b/frontend/src/views/ThroughputReportView.spec.ts
@@ -0,0 +1,130 @@
+import { describe, expect, test } from "vitest";
+import * as precondition from "../../test/preconditions";
+import { createTestingPinia } from "@pinia/testing";
+import { Transport } from "@/views/throughputreport/transport";
+import { makeDriverForTests, render, screen, userEvent } from "@component-test-utils";
+import { Driver } from "../../test/driver";
+import { disableMonitoring } from "../../test/drivers/vitest/setup";
+import makeRouter from "@/router";
+import { flushPromises, RouterLinkStub } from "@vue/test-utils";
+import ThroughputReportView from "@/views/ThroughputReportView.vue";
+import Toast from "vue-toastification";
+import { serviceControlWithThroughput } from "@/views/throughputreport/serviceControlWithThroughput";
+import { useServiceControlStore } from "@/stores/ServiceControlStore";
+import { setActivePinia } from "pinia";
+import { getDefaultConfig } from "@/defaultConfig";
+
+describe("EndpointsView tests", () => {
+ async function setup() {
+ const driver = makeDriverForTests();
+ setActivePinia(createTestingPinia({ stubActions: false }));
+
+ await driver.setUp(serviceControlWithThroughput);
+
+ return driver;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async function renderComponent(transport: Transport = Transport.MSMQ, preSetup: (driver: Driver) => Promise = () => Promise.resolve()) {
+ disableMonitoring();
+
+ const driver = await setup();
+ await preSetup(driver);
+
+ useServiceControlStore();
+
+ const el = document.createElement("div");
+ el.id = "modalDisplay";
+ document.body.appendChild(el);
+
+ const { debug } = render(ThroughputReportView, {
+ container: document.body,
+ global: {
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ plugins: [makeRouter(), Toast],
+ directives: {
+ // Add stub for tippy directive
+ tippy: () => {},
+ },
+ },
+ });
+ await flushPromises();
+
+ return { debug, driver };
+ }
+
+ describe("when minimum requirements", () => {
+ test("are met", async () => {
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(precondition.hasLicensingReportAvailable());
+ });
+
+ expect(screen.queryByText(/the minimum version of servicecontrol required to enable the usage feature is/i)).not.toBeInTheDocument();
+ });
+
+ test("are not met, requirements warning is displayed", async () => {
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(precondition.hasServiceControlMainInstance("1.0.0"));
+ await driver.setUp(precondition.hasLicensingReportAvailable());
+ });
+
+ expect(screen.getByText(/the minimum version of servicecontrol required to enable the usage feature is/i)).toBeInTheDocument();
+ });
+ });
+
+ describe("when report", () => {
+ test("is available", async () => {
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(precondition.hasLicensingReportAvailable());
+ });
+ expect(screen.getByRole("button", { name: /Download Report/i })).toBeEnabled();
+ });
+
+ test("is unavailable", async () => {
+ const reason = "report testing that is not available";
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(precondition.hasLicensingReportAvailable({ report_can_be_generated: false, reason: reason }));
+ });
+
+ expect(screen.getByRole("button", { name: /Download Report/i })).toBeDisabled();
+ expect(screen.getByText(reason)).toBeInTheDocument();
+ });
+ });
+
+ describe("when download report is clicked", () => {
+ test("and no warnings, download happens", async () => {
+ URL.createObjectURL = () => "";
+ const fileName = "hello_john.json";
+
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(precondition.hasLicensingReportAvailable());
+ await driver.setUp(precondition.hasLicensingEndpoints([{ name: "foo", is_known_endpoint: false, user_indicator: "something", max_daily_throughput: 0 }]));
+ driver.mockEndpoint(`${getDefaultConfig().service_control_url}licensing/report/file`, {
+ body: {},
+ headers: {
+ "Content-Disposition": `attachment; filename="${fileName}"`,
+ },
+ });
+ });
+
+ const use = userEvent.setup();
+
+ await use.click(screen.getByRole("button", { name: /Download Report/i }));
+ expect(screen.queryAllByText(new RegExp(`Please email '${fileName}' to your account manager`)).length).toBeGreaterThanOrEqual(1);
+ });
+
+ test("and there are warnings, dialog is displayed", async () => {
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(precondition.hasLicensingReportAvailable());
+ await driver.setUp(precondition.hasLicensingEndpoints([{ name: "foo", is_known_endpoint: false, user_indicator: "", max_daily_throughput: 0 }]));
+ });
+
+ const use = userEvent.setup();
+
+ await use.click(screen.getByRole("button", { name: /Download Report/i }));
+ expect(screen.getByText("Not all endpoints/queues have an Endpoint Type set")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/views/ThroughputReportView.vue b/frontend/src/views/ThroughputReportView.vue
new file mode 100644
index 0000000000..62561c15c1
--- /dev/null
+++ b/frontend/src/views/ThroughputReportView.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
Usage
+
+
+ {{ reportState?.reason }}
+ Download Report
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/throughputreport/EndpointsView.spec.ts b/frontend/src/views/throughputreport/EndpointsView.spec.ts
new file mode 100644
index 0000000000..4ec24b7476
--- /dev/null
+++ b/frontend/src/views/throughputreport/EndpointsView.spec.ts
@@ -0,0 +1,78 @@
+import { describe, expect, test } from "vitest";
+import * as precondition from "../../../test/preconditions";
+import { createTestingPinia } from "@pinia/testing";
+import { Transport } from "@/views/throughputreport/transport";
+import { makeDriverForTests, render, screen, userEvent } from "@component-test-utils";
+import { Driver } from "../../../test/driver";
+import { disableMonitoring } from "../../../test/drivers/vitest/setup";
+import makeRouter from "@/router";
+import { flushPromises, RouterLinkStub } from "@vue/test-utils";
+import EndpointsView from "./EndpointsView.vue";
+import { serviceControlWithThroughput } from "@/views/throughputreport/serviceControlWithThroughput";
+import { useServiceControlStore } from "@/stores/ServiceControlStore";
+import { setActivePinia } from "pinia";
+
+describe("EndpointsView tests", () => {
+ async function setup(transport: Transport) {
+ const driver = makeDriverForTests();
+ setActivePinia(createTestingPinia({ stubActions: false }));
+
+ await driver.setUp(serviceControlWithThroughput);
+ await driver.setUp(precondition.hasLicensingSettingTest({ transport }));
+
+ return driver;
+ }
+
+ async function renderComponent(transport: Transport = Transport.MSMQ, preSetup: (driver: Driver) => Promise = () => Promise.resolve()) {
+ disableMonitoring();
+
+ const driver = await setup(transport);
+ await preSetup(driver);
+
+ useServiceControlStore();
+
+ const { debug } = render(EndpointsView, {
+ global: {
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ plugins: [makeRouter()],
+ directives: {
+ // Add stub for tippy directive
+ tippy: () => {},
+ },
+ },
+ });
+ await flushPromises();
+
+ return { debug, driver };
+ }
+
+ test("instructions by default are not showing", async () => {
+ await renderComponent();
+
+ expect(screen.queryByText(/Show Endpoint Types meaning/i)).toBeInTheDocument();
+ });
+
+ test("show instructions", async () => {
+ await renderComponent();
+
+ const use = userEvent.setup();
+
+ await use.click(screen.getByRole("link", { name: /Show Endpoint Types meaning/i }));
+
+ expect(screen.queryByText(/Hide Endpoint Types meaning/i)).toBeInTheDocument();
+ });
+
+ test("broker displays the two tabs", async () => {
+ await renderComponent(Transport.AmazonSQS);
+
+ expect(screen.getByText(/Detected Broker Queues/i)).toBeInTheDocument();
+ });
+
+ test("non broker displays only one tabs", async () => {
+ await renderComponent();
+
+ expect(screen.queryByText(/Detected Broker Queues/i)).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/views/throughputreport/EndpointsView.vue b/frontend/src/views/throughputreport/EndpointsView.vue
new file mode 100644
index 0000000000..8bb1f99f8d
--- /dev/null
+++ b/frontend/src/views/throughputreport/EndpointsView.vue
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+ Set an Endpoint Type for all detected endpoints and broker queues with the most appropriate option.
+ Use the filters to bulk set the Endpoint Types on similar named endpoints/queues.
+ If the names of the endpoints/queues contain confidential or proprietary information, make sure you set up masking in Configuration .
+ {{ showLegend ? "Hide" : "Show" }} Endpoint Types meaning.
+
+
+
+
+
+
+
+ Detected Endpoints
+
+
+ Detected Broker Queues
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/views/throughputreport/LegendGatewayOrBridgeEndpoint.vue b/frontend/src/views/throughputreport/LegendGatewayOrBridgeEndpoint.vue
new file mode 100644
index 0000000000..40cae6e925
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendGatewayOrBridgeEndpoint.vue
@@ -0,0 +1,6 @@
+
+
+ This is either part of the Gateway infrastructure or part of the
+ Messaging Bridge infrastructure.
+
+
diff --git a/frontend/src/views/throughputreport/LegendNServiceBusEndpoint.vue b/frontend/src/views/throughputreport/LegendNServiceBusEndpoint.vue
new file mode 100644
index 0000000000..97183ed50e
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendNServiceBusEndpoint.vue
@@ -0,0 +1,6 @@
+
+
+ Known NServiceBus
+ Endpoint
+
+
diff --git a/frontend/src/views/throughputreport/LegendNServiceBusEndpointNoLongerInUse.vue b/frontend/src/views/throughputreport/LegendNServiceBusEndpointNoLongerInUse.vue
new file mode 100644
index 0000000000..742f3211bd
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendNServiceBusEndpointNoLongerInUse.vue
@@ -0,0 +1,3 @@
+
+ NServiceBus Endpoint that is no longer in use, usually this would have zero throughput
+
diff --git a/frontend/src/views/throughputreport/LegendNotNServiceBusEndpoint.vue b/frontend/src/views/throughputreport/LegendNotNServiceBusEndpoint.vue
new file mode 100644
index 0000000000..4ec9043290
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendNotNServiceBusEndpoint.vue
@@ -0,0 +1,3 @@
+
+ Not an NServiceBus Endpoint
+
diff --git a/frontend/src/views/throughputreport/LegendParticularPlatformEndpoint.vue b/frontend/src/views/throughputreport/LegendParticularPlatformEndpoint.vue
new file mode 100644
index 0000000000..a14e5bb52a
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendParticularPlatformEndpoint.vue
@@ -0,0 +1,3 @@
+
+ This is an infrastructure endpoint used by the Particular Platform
+
diff --git a/frontend/src/views/throughputreport/LegendPlannedToDecommission.vue b/frontend/src/views/throughputreport/LegendPlannedToDecommission.vue
new file mode 100644
index 0000000000..02393eb69f
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendPlannedToDecommission.vue
@@ -0,0 +1,3 @@
+
+ If the endpoint is planned to no longer be used in the next 30 days
+
diff --git a/frontend/src/views/throughputreport/LegendSendOnlyEndpoint.vue b/frontend/src/views/throughputreport/LegendSendOnlyEndpoint.vue
new file mode 100644
index 0000000000..40d0ba236c
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendSendOnlyEndpoint.vue
@@ -0,0 +1,3 @@
+
+ An endpoint that only sends messages and does not process any messages
+
diff --git a/frontend/src/views/throughputreport/LegendTransactionalSessionProcessorEndpoint.vue b/frontend/src/views/throughputreport/LegendTransactionalSessionProcessorEndpoint.vue
new file mode 100644
index 0000000000..27effc1500
--- /dev/null
+++ b/frontend/src/views/throughputreport/LegendTransactionalSessionProcessorEndpoint.vue
@@ -0,0 +1,3 @@
+
+ An endpoint that is only processing transactional session control messages
+
diff --git a/frontend/src/views/throughputreport/SetupView.spec.ts b/frontend/src/views/throughputreport/SetupView.spec.ts
new file mode 100644
index 0000000000..bc332cced3
--- /dev/null
+++ b/frontend/src/views/throughputreport/SetupView.spec.ts
@@ -0,0 +1,168 @@
+import { describe, expect, test } from "vitest";
+import * as precondition from "../../../test/preconditions";
+import { minimumSCVersionForThroughput } from "@/views/throughputreport/isThroughputSupported";
+import { createTestingPinia } from "@pinia/testing";
+import { Transport } from "@/views/throughputreport/transport";
+import { makeDriverForTests, render, screen } from "@component-test-utils";
+import { Driver } from "../../../test/driver";
+import { disableMonitoring } from "../../../test/drivers/vitest/setup";
+import SetupView from "./SetupView.vue";
+import { ConnectionSettingsTestResult } from "@/resources/ConnectionTestResults";
+import makeRouter from "@/router";
+import { flushPromises, RouterLinkStub } from "@vue/test-utils";
+import { serviceControlWithThroughput } from "@/views/throughputreport/serviceControlWithThroughput";
+import { useServiceControlStore } from "@/stores/ServiceControlStore";
+import { setActivePinia } from "pinia";
+
+describe("SetupView tests", () => {
+ async function setup() {
+ const driver = makeDriverForTests();
+ setActivePinia(createTestingPinia({ stubActions: false }));
+
+ await driver.setUp(serviceControlWithThroughput);
+
+ return driver;
+ }
+
+ async function renderComponent(transport: Transport = Transport.MSMQ, preSetup: (driver: Driver) => Promise = () => Promise.resolve()) {
+ const driver = await setup();
+
+ await driver.setUp(
+ precondition.hasLicensingSettingTest({
+ transport,
+ audit_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Audit diagnostics",
+ },
+ monitoring_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Monitoring diagnostics",
+ },
+ broker_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Broker diagnostics",
+ },
+ })
+ );
+
+ await preSetup(driver);
+
+ useServiceControlStore();
+
+ const { debug } = render(SetupView, {
+ global: {
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ plugins: [makeRouter()],
+ directives: {
+ // Add stub for tippy directive
+ tippy: () => {},
+ },
+ },
+ });
+ await flushPromises();
+
+ return { debug, driver };
+ }
+
+ describe("when minimum requirements", () => {
+ test("are met", async () => {
+ disableMonitoring();
+
+ await renderComponent();
+
+ expect(screen.queryByText(/the minimum version of servicecontrol required to enable the usage feature is \./i)).not.toBeInTheDocument();
+ });
+
+ test("are not met, requirements warning is displayed", async () => {
+ disableMonitoring();
+
+ await renderComponent(Transport.MSMQ, async (driver) => {
+ await driver.setUp(precondition.hasServiceControlMainInstance("1.0.0"));
+ });
+
+ expect(screen.getByText(/the minimum version of servicecontrol required to enable the usage feature is \./i)).toBeInTheDocument();
+ });
+ });
+
+ describe("when not a broker", () => {
+ test("without monitoring", async () => {
+ disableMonitoring();
+ await renderComponent();
+
+ expect(screen.getByText(/Successfully connected to Audit instance/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Successfully connected to Monitoring/i)).not.toBeInTheDocument();
+ });
+
+ test("with monitoring", async () => {
+ await renderComponent(Transport.MSMQ, async (driver) => {
+ await driver.setUp(precondition.serviceControlWithMonitoring);
+ await driver.setUp(precondition.hasServiceControlMainInstance(minimumSCVersionForThroughput));
+ await driver.setUp(
+ precondition.hasLicensingSettingTest({
+ transport: Transport.MSMQ,
+ audit_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Audit diagnostics",
+ },
+ monitoring_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Monitoring diagnostics",
+ },
+ broker_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Broker diagnostics",
+ },
+ })
+ );
+ });
+
+ expect(screen.getByText(/Successfully connected to Audit/i)).toBeInTheDocument();
+ expect(screen.getByText(/Successfully connected to Monitoring/i)).toBeInTheDocument();
+ });
+ });
+
+ describe("when a broker", () => {
+ test("display success", async () => {
+ disableMonitoring();
+ await renderComponent(Transport.AmazonSQS);
+
+ expect(screen.getByText(/Successfully connected to Amazon SQS for usage collection/i)).toBeInTheDocument();
+ });
+
+ test("display failure", async () => {
+ disableMonitoring();
+ await renderComponent(Transport.AmazonSQS, async (driver) => {
+ await driver.setUp(
+ precondition.hasLicensingSettingTest({
+ transport: Transport.AmazonSQS,
+ audit_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Audit diagnostics",
+ },
+ monitoring_connection_result: {
+ connection_successful: true,
+ connection_error_messages: [],
+ diagnostics: "Monitoring diagnostics",
+ },
+ broker_connection_result: {
+ connection_successful: false,
+ connection_error_messages: [],
+ diagnostics: "Broker diagnostics",
+ },
+ })
+ );
+ });
+
+ expect(screen.getByText(/The connection to Amazon SQS was not successful/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/views/throughputreport/SetupView.vue b/frontend/src/views/throughputreport/SetupView.vue
new file mode 100644
index 0000000000..62c71ef16f
--- /dev/null
+++ b/frontend/src/views/throughputreport/SetupView.vue
@@ -0,0 +1,107 @@
+
+
+