From db45a669e3778d8f9dac6bdf908306c58e5a1672 Mon Sep 17 00:00:00 2001 From: Wang Defa <61809431+wangdefaa@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:31:29 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=8F=90=E4=BA=A4=EF=BC=9AOC?= =?UTF-8?q?I=20=E9=9D=A2=E6=9D=BF=E5=89=8D=E7=AB=AF=EF=BC=88=E5=90=AB=20Ge?= =?UTF-8?q?nAI=20=E7=BD=91=E5=85=B3=E4=B8=80=E6=9C=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 2 + .gitignore | 6 + .prettierrc.json | 6 + env.d.ts | 10 + eslint.config.js | 16 + index.html | 13 + package-lock.json | 4724 +++++++++++++++++ package.json | 41 + public/favicon.svg | 7 + src/App.vue | 33 + src/api/aigateway.ts | 96 + src/api/analytics.ts | 28 + src/api/auth.ts | 151 + src/api/bootVolumes.ts | 59 + src/api/configs.ts | 121 + src/api/instances.ts | 380 ++ src/api/logevents.ts | 104 + src/api/mock.ts | 1716 ++++++ src/api/networks.ts | 66 + src/api/proxies.ts | 90 + src/api/request.ts | 66 + src/api/settings.ts | 152 + src/api/tasks.ts | 75 + src/api/tenant.ts | 344 ++ src/assets/main.css | 260 + src/components/AccountTypeBadge.vue | 26 + src/components/AppInputNumber.vue | 22 + src/components/AppLogo.vue | 30 + src/components/CostChart.vue | 60 + src/components/DeadPlaceholder.vue | 34 + src/components/DetailFieldList.vue | 57 + src/components/DetailHero.vue | 34 + src/components/EmptyCard.vue | 11 + src/components/FilePicker.vue | 112 + src/components/FootNote.vue | 5 + src/components/FormField.vue | 35 + src/components/FormModal.vue | 62 + src/components/FormSection.vue | 11 + src/components/FullPageBackdrop.vue | 78 + src/components/JsonBlock.vue | 121 + src/components/LifecycleBadge.vue | 22 + src/components/OauthProviderIcon.vue | 25 + src/components/OcidText.vue | 30 + src/components/PageHeader.vue | 16 + src/components/RenameModal.vue | 45 + src/components/StatusBadge.vue | 48 + src/components/TenantPicker.vue | 198 + src/components/ToggleCard.vue | 17 + src/components/ai/AiChannelPanel.vue | 250 + src/components/ai/AiKeyPanel.vue | 226 + src/components/config/EditConfigModal.vue | 194 + src/components/config/ImportConfigModal.vue | 213 + src/components/instance/AttachVnicModal.vue | 146 + src/components/instance/AttachVolumeModal.vue | 114 + src/components/instance/ConsoleConnModal.vue | 74 + .../instance/CreateInstanceModal.vue | 105 + .../instance/EditBootVolumeModal.vue | 85 + src/components/instance/InstanceSpecForm.vue | 535 ++ .../instance/InstanceTrafficPanel.vue | 140 + .../instance/ReplaceBootVolumeModal.vue | 94 + src/components/instance/ResizeModal.vue | 146 + .../instance/SerialConsolePanel.vue | 332 ++ src/components/instance/VnicPanel.vue | 235 + src/components/logs/AiCallLogPanel.vue | 293 + src/components/logs/LogEventPanel.vue | 280 + src/components/logs/SystemLogPanel.vue | 219 + src/components/network/CreateVcnModal.vue | 88 + src/components/proxy/ProxyPanel.vue | 386 ++ src/components/settings/AboutTab.vue | 123 + .../settings/AccountSecurityCard.vue | 739 +++ .../settings/NotifyTemplateModal.vue | 162 + .../storage/AttachBootVolumeModal.vue | 97 + .../storage/EditBootVolumeModal.vue | 110 + src/components/task/CronEditor.vue | 178 + src/components/task/TaskCard.vue | 191 + src/components/task/TaskFormModal.vue | 345 ++ src/components/task/TaskTypeIcon.vue | 50 + src/components/task/taskDisplay.ts | 91 + src/components/tenant/AuditLogTab.vue | 277 + src/components/tenant/CostTab.vue | 106 + src/components/tenant/IdpFormModal.vue | 218 + src/components/tenant/IdpTab.vue | 216 + src/components/tenant/MiscTab.vue | 409 ++ src/components/tenant/PolicyEditModal.vue | 94 + src/components/tenant/QuotaTab.vue | 233 + src/components/tenant/SubsTab.vue | 297 ++ .../tenant/SubscribeRegionModal.vue | 88 + src/components/tenant/UserFormModal.vue | 233 + src/components/tenant/UsersTab.vue | 176 + src/composables/cron.ts | 41 + src/composables/useActionLabel.ts | 83 + src/composables/useAsync.ts | 36 + src/composables/useAutoRefresh.ts | 20 + src/composables/useEventLabel.ts | 37 + src/composables/useFormat.ts | 62 + src/composables/useParticles.ts | 51 + src/composables/useProxyOptions.ts | 26 + src/composables/useRegionAlias.ts | 33 + src/composables/useToast.ts | 82 + src/composables/useTransientPoll.ts | 33 + src/layouts/AppLayout.vue | 231 + src/layouts/SidebarNav.vue | 240 + src/main.ts | 12 + src/router/index.ts | 103 + src/stores/app.ts | 37 + src/stores/auth.ts | 32 + src/stores/console.ts | 41 + src/stores/scope.ts | 186 + src/theme/naive.ts | 97 + src/theme/tokens.ts | 53 + src/types/api.ts | 969 ++++ src/types/novnc.d.ts | 15 + src/views/AiGatewayView.vue | 181 + src/views/BootVolumeListView.vue | 294 + src/views/InstanceDetailView.vue | 766 +++ src/views/InstanceListView.vue | 229 + src/views/LoginView.vue | 272 + src/views/LogsView.vue | 30 + src/views/NetworkListView.vue | 174 + src/views/OverviewView.vue | 243 + src/views/ProxyListView.vue | 22 + src/views/SettingsView.vue | 631 +++ src/views/TaskDetailView.vue | 323 ++ src/views/TaskListView.vue | 113 + src/views/TenantDetailView.vue | 165 + src/views/TenantListView.vue | 331 ++ src/views/VcnDetailView.vue | 308 ++ src/views/VncConsoleView.vue | 143 + src/views/errors/BlockedView.vue | 49 + src/views/errors/NotFoundView.vue | 28 + src/views/errors/error-page.css | 70 + tsconfig.app.json | 15 + tsconfig.json | 7 + tsconfig.node.json | 12 + vite.config.ts | 41 + 135 files changed, 25220 insertions(+) create mode 100644 .env.development create mode 100644 .gitignore create mode 100644 .prettierrc.json create mode 100644 env.d.ts create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/favicon.svg create mode 100644 src/App.vue create mode 100644 src/api/aigateway.ts create mode 100644 src/api/analytics.ts create mode 100644 src/api/auth.ts create mode 100644 src/api/bootVolumes.ts create mode 100644 src/api/configs.ts create mode 100644 src/api/instances.ts create mode 100644 src/api/logevents.ts create mode 100644 src/api/mock.ts create mode 100644 src/api/networks.ts create mode 100644 src/api/proxies.ts create mode 100644 src/api/request.ts create mode 100644 src/api/settings.ts create mode 100644 src/api/tasks.ts create mode 100644 src/api/tenant.ts create mode 100644 src/assets/main.css create mode 100644 src/components/AccountTypeBadge.vue create mode 100644 src/components/AppInputNumber.vue create mode 100644 src/components/AppLogo.vue create mode 100644 src/components/CostChart.vue create mode 100644 src/components/DeadPlaceholder.vue create mode 100644 src/components/DetailFieldList.vue create mode 100644 src/components/DetailHero.vue create mode 100644 src/components/EmptyCard.vue create mode 100644 src/components/FilePicker.vue create mode 100644 src/components/FootNote.vue create mode 100644 src/components/FormField.vue create mode 100644 src/components/FormModal.vue create mode 100644 src/components/FormSection.vue create mode 100644 src/components/FullPageBackdrop.vue create mode 100644 src/components/JsonBlock.vue create mode 100644 src/components/LifecycleBadge.vue create mode 100644 src/components/OauthProviderIcon.vue create mode 100644 src/components/OcidText.vue create mode 100644 src/components/PageHeader.vue create mode 100644 src/components/RenameModal.vue create mode 100644 src/components/StatusBadge.vue create mode 100644 src/components/TenantPicker.vue create mode 100644 src/components/ToggleCard.vue create mode 100644 src/components/ai/AiChannelPanel.vue create mode 100644 src/components/ai/AiKeyPanel.vue create mode 100644 src/components/config/EditConfigModal.vue create mode 100644 src/components/config/ImportConfigModal.vue create mode 100644 src/components/instance/AttachVnicModal.vue create mode 100644 src/components/instance/AttachVolumeModal.vue create mode 100644 src/components/instance/ConsoleConnModal.vue create mode 100644 src/components/instance/CreateInstanceModal.vue create mode 100644 src/components/instance/EditBootVolumeModal.vue create mode 100644 src/components/instance/InstanceSpecForm.vue create mode 100644 src/components/instance/InstanceTrafficPanel.vue create mode 100644 src/components/instance/ReplaceBootVolumeModal.vue create mode 100644 src/components/instance/ResizeModal.vue create mode 100644 src/components/instance/SerialConsolePanel.vue create mode 100644 src/components/instance/VnicPanel.vue create mode 100644 src/components/logs/AiCallLogPanel.vue create mode 100644 src/components/logs/LogEventPanel.vue create mode 100644 src/components/logs/SystemLogPanel.vue create mode 100644 src/components/network/CreateVcnModal.vue create mode 100644 src/components/proxy/ProxyPanel.vue create mode 100644 src/components/settings/AboutTab.vue create mode 100644 src/components/settings/AccountSecurityCard.vue create mode 100644 src/components/settings/NotifyTemplateModal.vue create mode 100644 src/components/storage/AttachBootVolumeModal.vue create mode 100644 src/components/storage/EditBootVolumeModal.vue create mode 100644 src/components/task/CronEditor.vue create mode 100644 src/components/task/TaskCard.vue create mode 100644 src/components/task/TaskFormModal.vue create mode 100644 src/components/task/TaskTypeIcon.vue create mode 100644 src/components/task/taskDisplay.ts create mode 100644 src/components/tenant/AuditLogTab.vue create mode 100644 src/components/tenant/CostTab.vue create mode 100644 src/components/tenant/IdpFormModal.vue create mode 100644 src/components/tenant/IdpTab.vue create mode 100644 src/components/tenant/MiscTab.vue create mode 100644 src/components/tenant/PolicyEditModal.vue create mode 100644 src/components/tenant/QuotaTab.vue create mode 100644 src/components/tenant/SubsTab.vue create mode 100644 src/components/tenant/SubscribeRegionModal.vue create mode 100644 src/components/tenant/UserFormModal.vue create mode 100644 src/components/tenant/UsersTab.vue create mode 100644 src/composables/cron.ts create mode 100644 src/composables/useActionLabel.ts create mode 100644 src/composables/useAsync.ts create mode 100644 src/composables/useAutoRefresh.ts create mode 100644 src/composables/useEventLabel.ts create mode 100644 src/composables/useFormat.ts create mode 100644 src/composables/useParticles.ts create mode 100644 src/composables/useProxyOptions.ts create mode 100644 src/composables/useRegionAlias.ts create mode 100644 src/composables/useToast.ts create mode 100644 src/composables/useTransientPoll.ts create mode 100644 src/layouts/AppLayout.vue create mode 100644 src/layouts/SidebarNav.vue create mode 100644 src/main.ts create mode 100644 src/router/index.ts create mode 100644 src/stores/app.ts create mode 100644 src/stores/auth.ts create mode 100644 src/stores/console.ts create mode 100644 src/stores/scope.ts create mode 100644 src/theme/naive.ts create mode 100644 src/theme/tokens.ts create mode 100644 src/types/api.ts create mode 100644 src/types/novnc.d.ts create mode 100644 src/views/AiGatewayView.vue create mode 100644 src/views/BootVolumeListView.vue create mode 100644 src/views/InstanceDetailView.vue create mode 100644 src/views/InstanceListView.vue create mode 100644 src/views/LoginView.vue create mode 100644 src/views/LogsView.vue create mode 100644 src/views/NetworkListView.vue create mode 100644 src/views/OverviewView.vue create mode 100644 src/views/ProxyListView.vue create mode 100644 src/views/SettingsView.vue create mode 100644 src/views/TaskDetailView.vue create mode 100644 src/views/TaskListView.vue create mode 100644 src/views/TenantDetailView.vue create mode 100644 src/views/TenantListView.vue create mode 100644 src/views/VcnDetailView.vue create mode 100644 src/views/VncConsoleView.vue create mode 100644 src/views/errors/BlockedView.vue create mode 100644 src/views/errors/NotFoundView.vue create mode 100644 src/views/errors/error-page.css create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..a62bf31 --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +# 开发期默认走内置 mock;对接真实后端时改为 0 并启动 oci-portal(:8080) +VITE_MOCK=0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a739064 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +*.local +.DS_Store +npm-debug.log* +.playwright-cli diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..3ee2d11 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..4287929 --- /dev/null +++ b/env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** 为 '1' 时 api 层返回内置 mock 数据,不请求后端 */ + readonly VITE_MOCK: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9429fcd --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import pluginVue from 'eslint-plugin-vue' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default defineConfigWithVueTs( + { ignores: ['dist/**', 'node_modules/**'] }, + pluginVue.configs['flat/recommended'], + vueTsConfigs.recommended, + skipFormatting, + { + rules: { + '@typescript-eslint/no-explicit-any': 'error', + 'vue/multi-word-component-names': 'error', + }, + }, +) diff --git a/index.html b/index.html new file mode 100644 index 0000000..765d5b6 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + OCI Portal + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..87591d9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4724 @@ +{ + "name": "oci-portal-dash", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "oci-portal-dash", + "version": "0.1.0", + "dependencies": { + "@novnc/novnc": "^1.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "echarts": "^5.6.0", + "naive-ui": "^2.41.0", + "pinia": "^2.3.0", + "vue": "^3.5.13", + "vue-echarts": "^7.0.3", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.10.7", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-prettier": "^10.1.0", + "@vue/eslint-config-typescript": "^14.3.0", + "@vue/tsconfig": "^0.7.0", + "eslint": "^9.18.0", + "eslint-plugin-vue": "^9.32.0", + "prettier": "^3.4.2", + "tailwindcss": "^4.0.0", + "typescript": "~5.7.3", + "vite": "^6.0.7", + "vue-tsc": "^2.2.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@css-render/plugin-bem": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz", + "integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==", + "license": "MIT", + "peerDependencies": { + "css-render": "~0.15.14" + } + }, + "node_modules/@css-render/vue3-ssr": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz", + "integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.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.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "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.1", + "minimatch": "^3.1.5", + "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/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/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "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/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "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/@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/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, + "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/@novnc/novnc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@novnc/novnc/-/novnc-1.7.0.tgz", + "integrity": "sha512-ucEJOx4T2avIRCleodk7YobZj5O2Ga2AeLfQ69A/yjG9HHba2+PDgwSkN3FttrmG+70ZGx21sElNFouK13RzyA==", + "license": "MPL-2.0" + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tsconfig/node22": { + "version": "22.0.5", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.5.tgz", + "integrity": "sha512-hLf2ld+sYN/BtOJjHUWOk568dvjFQkHnLNa6zce25GIH+vxKfvTgm3qpaH6ToF5tu/NN0IH66s+Bb5wElHrLcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "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/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.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.62.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", + "debug": "^4.4.3" + }, + "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.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" + }, + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", + "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.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", + "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.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.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.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.62.1", + "eslint-visitor-keys": "^5.0.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/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "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/eslint-config-prettier": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", + "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2" + }, + "peerDependencies": { + "eslint": ">= 8.21.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "14.9.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.9.0.tgz", + "integrity": "sha512-E3j9hDlfVf10F30MRcLTPY2IIhWIx1nsvkVukk14kTcuA+oBVot9zsP1hzsO+PAMDxV3Fd9FimBJtUBNBL5KFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.60.0", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.60.0", + "vue-eslint-parser": "^10.4.0" + }, + "bin": { + "vue-eslint-config-typescript": "dist/bin.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0 || ^10.0.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/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/@vue/language-core/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "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/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "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": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "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/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/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "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/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/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/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/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/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-render": { + "version": "0.15.14", + "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.14.tgz", + "integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "~0.8.0", + "csstype": "~3.0.5" + } + }, + "node_modules/css-render/node_modules/csstype": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", + "integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==", + "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/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/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/echarts": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz", + "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "5.6.1" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@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.14.0", + "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.5", + "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", + "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.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "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-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "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": "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/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/eslint/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/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/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/espree/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/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "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": "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/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/evtd": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/evtd/-/evtd-0.2.4.tgz", + "integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==", + "license": "MIT" + }, + "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.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "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.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "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/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/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.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/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-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-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/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/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/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/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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "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/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/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/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/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/naive-ui": { + "version": "2.44.1", + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.44.1.tgz", + "integrity": "sha512-reo8Esw0p58liZwbUutC7meW24Xbn3EwNv91zReWKm2W4JPu+zfgJRn/F7aO0BFmvN+h2brA2M5lRvYqLq4kuA==", + "license": "MIT", + "dependencies": { + "@css-render/plugin-bem": "^0.15.14", + "@css-render/vue3-ssr": "^0.15.14", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "async-validator": "^4.2.5", + "css-render": "^0.15.14", + "csstype": "^3.1.3", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "evtd": "^0.2.4", + "highlight.js": "^11.8.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "seemly": "^0.3.10", + "treemate": "^0.3.11", + "vdirs": "^0.1.8", + "vooks": "^0.2.12", + "vueuc": "^0.4.65" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/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/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/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/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/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/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.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "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.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", + "dev": true, + "license": "MIT", + "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.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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/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/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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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/seemly": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/seemly/-/seemly-0.3.10.tgz", + "integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "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/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/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/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/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/treemate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/treemate/-/treemate-0.3.11.tgz", + "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "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": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", + "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.62.1", + "@typescript-eslint/parser": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1" + }, + "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 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "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/vdirs": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz", + "integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==", + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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/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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vooks": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/vooks/-/vooks-0.2.12.tgz", + "integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==", + "license": "MIT", + "dependencies": { + "evtd": "^0.2.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "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.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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-echarts": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/vue-echarts/-/vue-echarts-7.0.3.tgz", + "integrity": "sha512-/jSxNwOsw5+dYAUcwSfkLwKPuzTQ0Cepz1LxCOpj2QcHrrmUa/Ql0eQqMmc1rTPQVrh2JQ29n2dhq75ZcHvRDw==", + "license": "MIT", + "dependencies": { + "vue-demi": "^0.13.11" + }, + "peerDependencies": { + "@vue/runtime-core": "^3.0.0", + "echarts": "^5.5.1", + "vue": "^2.7.0 || ^3.1.1" + }, + "peerDependenciesMeta": { + "@vue/runtime-core": { + "optional": true + } + } + }, + "node_modules/vue-echarts/node_modules/vue-demi": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz", + "integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==", + "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.4.1", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.1.tgz", + "integrity": "sha512-Gk6gRDj0n/fkRa3C3l0bBheoBckUq/Rs0F/TvMWIS6nzzx67amAViMe9CkNgsP2tXyQONvGiHQESHwFtZ3aYDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.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 || ^10.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vueuc": { + "version": "0.4.65", + "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.65.tgz", + "integrity": "sha512-lXuMl+8gsBmruudfxnMF9HW4be8rFziylXFu1VHVNbLVhRTXXV4njvpRuJapD/8q+oFEMSfQMH16E/85VoWRyQ==", + "license": "MIT", + "dependencies": { + "@css-render/vue3-ssr": "^0.15.10", + "@juggle/resize-observer": "^3.3.1", + "css-render": "^0.15.10", + "evtd": "^0.2.4", + "seemly": "^0.3.6", + "vdirs": "^0.1.4", + "vooks": "^0.2.4" + }, + "peerDependencies": { + "vue": "^3.0.11" + } + }, + "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/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/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/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/zrender": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", + "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3fa3f4d --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "oci-portal-dash", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build", + "preview": "vite preview", + "typecheck": "vue-tsc --noEmit", + "lint": "eslint . --fix", + "format": "prettier --write src/" + }, + "dependencies": { + "@novnc/novnc": "^1.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "echarts": "^5.6.0", + "naive-ui": "^2.41.0", + "pinia": "^2.3.0", + "vue": "^3.5.13", + "vue-echarts": "^7.0.3", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "@tsconfig/node22": "^22.0.0", + "@types/node": "^22.10.7", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/eslint-config-prettier": "^10.1.0", + "@vue/eslint-config-typescript": "^14.3.0", + "@vue/tsconfig": "^0.7.0", + "eslint": "^9.18.0", + "eslint-plugin-vue": "^9.32.0", + "prettier": "^3.4.2", + "tailwindcss": "^4.0.0", + "typescript": "~5.7.3", + "vite": "^6.0.7", + "vue-tsc": "^2.2.0" + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..164709b --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,7 @@ + + + + + O! + + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..b801326 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,33 @@ + + + diff --git a/src/api/aigateway.ts b/src/api/aigateway.ts new file mode 100644 index 0000000..5d0a442 --- /dev/null +++ b/src/api/aigateway.ts @@ -0,0 +1,96 @@ +import { request } from './request' +import type { AiCallLog, AiChannel, AiContentLog, AiKey, AiModel, AiModelCacheItem } from '@/types/api' + +// ---- 网关密钥 ---- + +export function listAiKeys(): Promise { + return request<{ items: AiKey[] }>('/ai-keys').then((r) => r.items) +} + +export interface CreateAiKeyRequest { + name: string + value?: string + group?: string +} + +/** 创建密钥;key 为明文,仅本次响应返回 */ +export function createAiKey(body: CreateAiKeyRequest): Promise<{ key: string; item: AiKey }> { + return request('/ai-keys', { method: 'POST', body }) +} + +export function updateAiKey( + id: number, + body: { name?: string; enabled?: boolean; group?: string }, +): Promise { + return request(`/ai-keys/${id}`, { method: 'PUT', body }) +} + +export function deleteAiKey(id: number): Promise { + return request(`/ai-keys/${id}`, { method: 'DELETE' }) +} + +/** 设置密钥内容日志窗口:0 关闭,>0 从现在起开启 N 小时(上限 168 = 7 天),返回最新密钥 */ +export function updateAiKeyContentLog(id: number, hours: number): Promise { + return request(`/ai-keys/${id}/content-log`, { method: 'PUT', body: { hours } }) +} + +// ---- 渠道(号池)---- + +export function listAiChannels(): Promise { + return request<{ items: AiChannel[] }>('/ai-channels').then((r) => r.items) +} + +export interface AiChannelInput { + ociConfigId?: number + region?: string + name?: string + group?: string + enabled?: boolean + priority?: number + weight?: number +} + +export function createAiChannel(body: AiChannelInput): Promise { + return request('/ai-channels', { method: 'POST', body }) +} + +export function updateAiChannel(id: number, body: AiChannelInput): Promise { + return request(`/ai-channels/${id}`, { method: 'PUT', body }) +} + +export function deleteAiChannel(id: number): Promise { + return request(`/ai-channels/${id}`, { method: 'DELETE' }) +} + +export function probeAiChannel(id: number): Promise { + return request(`/ai-channels/${id}/probe`, { method: 'POST' }) +} + +export function syncAiChannelModels(id: number): Promise { + return request<{ items: AiModelCacheItem[] }>(`/ai-channels/${id}/sync-models`, { + method: 'POST', + }).then((r) => r.items) +} + +// ---- 聚合模型与调用日志 ---- + +export function listAiModels(): Promise { + return request<{ items: AiModel[] }>('/ai-models').then((r) => r.items) +} + +export function listAiCallLogs(params: { + page: number + size: number +}): Promise<{ items: AiCallLog[]; total: number }> { + return request('/ai-logs', { query: params }) +} + +/** 内容日志(红线例外);keyId / callLogId 可选过滤 */ +export function listAiContentLogs(params: { + page: number + size: number + keyId?: number + callLogId?: number +}): Promise<{ items: AiContentLog[]; total: number }> { + return request('/ai-content-logs', { query: params }) +} diff --git a/src/api/analytics.ts b/src/api/analytics.ts new file mode 100644 index 0000000..c7399ce --- /dev/null +++ b/src/api/analytics.ts @@ -0,0 +1,28 @@ +import { mockCosts, mockTraffic } from './mock' +import { mockOn, mocked, request } from './request' +import type { CostItem, InstanceTraffic } from '@/types/api' + +export function getInstanceTraffic( + cfgId: number, + instanceId: string, + days = 30, + region?: string, +): Promise { + if (mockOn) return mocked(mockTraffic, 800) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/traffic`, { + query: { days, region }, + }) +} + +export interface CostQuery { + startTime?: string + endTime?: string + granularity?: 'DAILY' | 'MONTHLY' + queryType?: 'COST' | 'USAGE' + groupBy?: 'service' | 'skuName' | 'region' | 'compartmentName' +} + +export function getCosts(cfgId: number, query: CostQuery = {}): Promise { + if (mockOn) return mocked(mockCosts) + return request(`/oci-configs/${cfgId}/costs`, { query: { ...query } }) +} diff --git a/src/api/auth.ts b/src/api/auth.ts new file mode 100644 index 0000000..4289b8e --- /dev/null +++ b/src/api/auth.ts @@ -0,0 +1,151 @@ +import { mockOn, mocked, request } from './request' +import type { + CredentialsInfo, + OauthProviderInfo, + LoginRequest, + LoginResponse, + OAuthSettings, + TotpSetup, + TotpStatus, + UpdateCredentialsRequest, + UpdateOAuthRequest, + UserIdentityInfo, +} from '@/types/api' + +export function login(body: LoginRequest): Promise { + if (mockOn) { + if (body.password === 'wrong') return Promise.reject(new Error('用户名或密码错误')) + return mocked({ token: 'mock-token', expiresAt: '2099-01-01T00:00:00Z' }, 400) + } + return request('/auth/login', { method: 'POST', body }) +} + +/** 服务端登出:当前令牌拉黑至自然过期;调用方无论成败都应清本地会话 */ +export function logout(): Promise { + if (mockOn) return mocked(undefined, 100) + return request('/auth/logout', { method: 'POST' }) +} + +// ---- 登录凭据 ---- + +export function getCredentials(): Promise { + if (mockOn) return mocked({ username: 'admin', passwordLoginDisabled: false }) + return request('/auth/credentials') +} + +/** 修改用户名 / 密码;成功后旧 token 随即失效,调用方应登出重登 */ +export function updateCredentials(body: UpdateCredentialsRequest): Promise { + if (mockOn) return mocked(undefined, 400) + return request('/auth/credentials', { method: 'PUT', body }) +} + +/** 保存密码登录禁用开关;开启要求至少绑定一个外部身份(后端 409) */ +export function updatePasswordLogin(disabled: boolean): Promise { + if (mockOn) return mocked(undefined, 300) + return request('/auth/password-login', { method: 'PUT', body: { disabled } }) +} + +// ---- 两步验证(TOTP) ---- + +export function getTotpStatus(): Promise { + if (mockOn) return mocked({ enabled: false }) + return request('/auth/totp') +} + +/** 生成待激活密钥;10 分钟内输入验证码激活,重复调用覆盖旧暂存 */ +export function setupTotp(): Promise { + if (mockOn) + return mocked({ + secret: 'JBSWY3DPEHPK3PXP', + otpauthUri: 'otpauth://totp/oci-portal:admin?secret=JBSWY3DPEHPK3PXP&issuer=oci-portal', + }) + return request('/auth/totp/setup', { method: 'POST' }) +} + +export function activateTotp(code: string): Promise { + if (mockOn) return mocked(undefined, 300) + return request('/auth/totp/activate', { method: 'POST', body: { code } }) +} + +/** 停用两步验证;密码或当前验证码任一确认 */ +export function disableTotp(body: { password?: string; code?: string }): Promise { + if (mockOn) return mocked(undefined, 300) + return request('/auth/totp/disable', { method: 'POST', body }) +} + +// ---- 外部身份(OAuth) ---- + +/** 可登录的 provider 列表(登录页公开接口;已禁用的不返回); + * passwordLoginDisabled 为 true 且有可用 provider 时,登录页隐藏密码表单 */ +export function getOauthProviders(): Promise<{ + providers: OauthProviderInfo[] + passwordLoginDisabled: boolean +}> { + if (mockOn) + return mocked({ + providers: [ + { provider: 'github', displayName: 'GitHub' }, + { provider: 'oidc', displayName: 'OIDC SSO' }, + ], + passwordLoginDisabled: false, + }) + return request('/auth/oauth/providers') +} + +/** 获取授权跳转 URL;bind 模式要求已登录(带 JWT) */ +export function getOauthAuthorizeUrl( + provider: string, + mode: 'login' | 'bind', +): Promise<{ url: string }> { + if (mockOn) return mocked({ url: 'https://example.com/oauth/authorize?mock=1' }) + return request(`/auth/oauth/${provider}/authorize`, { query: { mode } }) +} + +export function listIdentities(): Promise<{ items: UserIdentityInfo[] }> { + if (mockOn) + return mocked({ + items: [ + { id: 1, provider: 'github', display: 'octocat', createdAt: '2026-07-07T10:00:00+08:00' }, + ], + }) + return request('/auth/identities') +} + +export function unbindIdentity(id: number): Promise { + if (mockOn) return mocked(undefined, 300) + return request(`/auth/identities/${id}`, { method: 'DELETE' }) +} + +// ---- OAuth provider 配置(设置页) ---- + +export function getOAuthSettings(): Promise { + if (mockOn) + return mocked({ + oidcIssuer: '', + oidcClientId: '', + oidcSecretSet: false, + oidcDisplayName: '', + oidcDisabled: false, + githubClientId: '', + githubSecretSet: false, + githubDisplayName: '', + githubDisabled: false, + }) + return request('/settings/oauth') +} + +export function updateOAuthSettings(body: UpdateOAuthRequest): Promise { + if (mockOn) + return mocked({ + oidcIssuer: body.oidcIssuer, + oidcClientId: body.oidcClientId, + oidcSecretSet: !!body.oidcClientSecret, + oidcDisplayName: body.oidcDisplayName, + oidcDisabled: body.oidcDisabled, + githubClientId: body.githubClientId, + githubSecretSet: !!body.githubClientSecret, + githubDisplayName: body.githubDisplayName, + githubDisabled: body.githubDisabled, + }) + return request('/settings/oauth', { method: 'PUT', body }) +} diff --git a/src/api/bootVolumes.ts b/src/api/bootVolumes.ts new file mode 100644 index 0000000..30154a4 --- /dev/null +++ b/src/api/bootVolumes.ts @@ -0,0 +1,59 @@ +import { mockBootVolumes } from './mock' +import { mockOn, mocked, request } from './request' +import type { BootVolume, UpdateBootVolumeRequest } from '@/types/api' + +export function listBootVolumes( + cfgId: number, + availabilityDomain?: string, + compartmentId?: string, + region?: string, +): Promise { + if (mockOn) return mocked(compartmentId ? [] : (mockBootVolumes[cfgId] ?? [])) + return request(`/oci-configs/${cfgId}/boot-volumes`, { + query: { availabilityDomain, compartmentId, region }, + }) +} + +export function getBootVolume( + cfgId: number, + bootVolumeId: string, + region?: string, +): Promise { + if (mockOn) { + const found = Object.values(mockBootVolumes) + .flat() + .find((b) => b.id === bootVolumeId) + return found ? mocked(found) : Promise.reject(new Error('引导卷不存在')) + } + return request(`/oci-configs/${cfgId}/boot-volumes/${encodeURIComponent(bootVolumeId)}`, { + query: { region }, + }) +} + +export function updateBootVolume( + cfgId: number, + bootVolumeId: string, + body: UpdateBootVolumeRequest, + region?: string, +): Promise { + if (mockOn) { + const base = Object.values(mockBootVolumes).flat()[0] + return mocked({ ...base, ...body }, 700) + } + return request(`/oci-configs/${cfgId}/boot-volumes/${encodeURIComponent(bootVolumeId)}`, { + method: 'PUT', + body: { region: region ?? '', ...body }, + }) +} + +export function deleteBootVolume( + cfgId: number, + bootVolumeId: string, + region?: string, +): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/boot-volumes/${encodeURIComponent(bootVolumeId)}`, { + method: 'DELETE', + query: { region }, + }) +} diff --git a/src/api/configs.ts b/src/api/configs.ts new file mode 100644 index 0000000..a8206f6 --- /dev/null +++ b/src/api/configs.ts @@ -0,0 +1,121 @@ +import { mockCompartments, mockConfigs, mockOverview, mockRegionSubs, mockRegions } from './mock' +import { mockOn, mocked, request } from './request' +import type { + Compartment, + ImportConfigRequest, + OciConfig, + OciConfigSummary, + Overview, + RegionInfo, + RegionSubscription, + UpdateConfigRequest, + VerifyResult, +} from '@/types/api' + +export function getOverview(): Promise { + if (mockOn) return mocked(mockOverview) + return request('/overview') +} + +export function listCompartments(id: number): Promise { + if (mockOn) return mocked(mockCompartments[id] ?? []) + return request(`/oci-configs/${id}/compartments`) +} + +/** 列表返回瘦身摘要,订阅 / 促销等详情字段走 getConfig 单查 */ +export function listConfigs(): Promise { + if (mockOn) return mocked(mockConfigs) + return request('/oci-configs') +} + +export function getConfig(id: number): Promise { + if (mockOn) { + const found = mockConfigs.find((c) => c.id === id) + return found ? mocked(found) : Promise.reject(new Error('配置不存在')) + } + return request(`/oci-configs/${id}`) +} + +export function importConfig(body: ImportConfigRequest): Promise { + if (mockOn) { + const id = Math.max(...mockConfigs.map((c) => c.id)) + 1 + const created = { ...mockConfigs[0], id, alias: body.alias, accountType: 'unknown' as const } + mockConfigs.push(created) + return mocked(created, 1200) + } + return request('/oci-configs', { method: 'POST', body }) +} + +export function updateConfig(id: number, body: UpdateConfigRequest): Promise { + if (mockOn) { + const found = mockConfigs.find((c) => c.id === id) + if (!found) return Promise.reject(new Error('配置不存在')) + if (body.alias) found.alias = body.alias + if (body.group !== undefined) found.group = body.group + if (body.region) found.region = body.region + if (body.multiRegion !== undefined) found.multiRegion = body.multiRegion + if (body.multiCompartment !== undefined) found.multiCompartment = body.multiCompartment + return mocked({ config: found, changes: {} }, 900) + } + return request(`/oci-configs/${id}`, { method: 'PUT', body }) +} + +export function verifyConfig(id: number): Promise { + if (mockOn) return mocked({ config: mockConfigs.find((c) => c.id === id) ?? mockConfigs[0], changes: {} }, 800) + return request(`/oci-configs/${id}/verify`, { method: 'POST' }) +} + +export function deleteConfig(id: number): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${id}`, { method: 'DELETE' }) +} + +export function listRegions(): Promise { + if (mockOn) return mocked(mockRegions) + return request('/regions') +} + +export function listRegionSubscriptions(id: number): Promise { + if (mockOn) return mocked(mockRegionSubs) + return request(`/oci-configs/${id}/region-subscriptions`) +} + +export function subscribeRegion(id: number, regionKey: string): Promise { + if (mockOn) + return mocked([ + ...mockRegionSubs, + { key: regionKey, name: '', status: 'IN_PROGRESS', isHomeRegion: false, alias: '' }, + ]) + return request(`/oci-configs/${id}/region-subscriptions`, { method: 'POST', body: { regionKey } }) +} + +/** 筛选器用区域列表(缓存):未开多区域支持只含默认区域;有非 READY 时服务端实时刷新 */ +export function listCachedRegions(id: number): Promise { + if (mockOn) { + const cfg = mockConfigs.find((c) => c.id === id) + if (!cfg) return mocked([]) + if (!cfg.multiRegion) { + const info = mockRegions.find((r) => r.name === cfg.region) + return mocked([ + { + key: info?.key ?? cfg.homeRegionKey, + name: cfg.region, + status: 'READY', + isHomeRegion: true, + alias: info?.alias ?? '', + }, + ]) + } + return mocked(mockRegionSubs) + } + return request(`/oci-configs/${id}/cached-regions`) +} + +/** 筛选器用区间列表(缓存):未开多区间支持返回空数组(前端锁定根) */ +export function listCachedCompartments(id: number): Promise { + if (mockOn) { + const cfg = mockConfigs.find((c) => c.id === id) + return mocked(cfg?.multiCompartment ? (mockCompartments[id] ?? []) : []) + } + return request(`/oci-configs/${id}/cached-compartments`) +} diff --git a/src/api/instances.ts b/src/api/instances.ts new file mode 100644 index 0000000..88801db --- /dev/null +++ b/src/api/instances.ts @@ -0,0 +1,380 @@ +import { + mockAds, + mockBvAttachments, + mockConsoleConns, + mockImages, + mockInstances, + mockShapes, + mockVolAttachments, + mockVolumes, +} from './mock' +import { mockOn, mocked, request } from './request' +import type { + AttachVnicRequest, + BootVolumeAttachment, + ComputeShape, + ConsoleConnection, + CreateInstanceRequest, + CreateInstancesResponse, + ImageInfo, + Instance, + PowerAction, + UpdateInstanceRequest, + Vnic, + Volume, + VolumeAttachment, +} from '@/types/api' + +// ---- 创建实例前置查询 ---- +export interface ImagesQuery { + region?: string + operatingSystem?: string + shape?: string +} + +export function listImages(cfgId: number, query: ImagesQuery = {}): Promise { + if (mockOn) return mocked(mockImages) + return request(`/oci-configs/${cfgId}/images`, { query: { ...query } }) +} + +export function getImage(cfgId: number, imageId: string, region?: string): Promise { + if (mockOn) { + const found = mockImages.find((i) => i.id === imageId) ?? { + ...mockImages[0], + id: imageId, + displayName: 'Canonical-Ubuntu-24.04-aarch64-2026.05.20-0', + } + return mocked(found) + } + return request(`/oci-configs/${cfgId}/images/${encodeURIComponent(imageId)}`, { + query: { region }, + }) +} + +export function listAvailabilityDomains(cfgId: number, region?: string): Promise { + if (mockOn) return mocked(mockAds) + return request(`/oci-configs/${cfgId}/availability-domains`, { query: { region } }) +} + +export function listShapes( + cfgId: number, + region?: string, + availabilityDomain?: string, +): Promise { + if (mockOn) return mocked(mockShapes) + return request(`/oci-configs/${cfgId}/shapes`, { query: { region, availabilityDomain } }) +} + +export function listVolumes( + cfgId: number, + availabilityDomain?: string, + compartmentId?: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockVolumes[cfgId] ?? []) + return request(`/oci-configs/${cfgId}/volumes`, { + query: { availabilityDomain, compartmentId, region }, + }) +} + +export function listInstances( + cfgId: number, + region?: string, + compartmentId?: string, +): Promise { + if (mockOn) return mocked(compartmentId ? [] : (mockInstances[cfgId] ?? [])) + return request(`/oci-configs/${cfgId}/instances`, { query: { region, compartmentId } }) +} + +export function getInstance(cfgId: number, instanceId: string, region?: string): Promise { + if (mockOn) { + const all = Object.values(mockInstances).flat() + const found = all.find((i) => i.id === instanceId) + return found ? mocked(found) : Promise.reject(new Error('实例不存在')) + } + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, { + query: { region }, + }) +} + +export function createInstances( + cfgId: number, + body: CreateInstanceRequest, +): Promise { + if (mockOn) return mocked({ instances: [mockInstances[1][0]], errors: [] }, 1200) + return request(`/oci-configs/${cfgId}/instances`, { method: 'POST', body }) +} + +export function updateInstance( + cfgId: number, + instanceId: string, + body: UpdateInstanceRequest, + region?: string, +): Promise { + if (mockOn) return mocked({ ...mockInstances[1][0], ...body }) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, { + method: 'PUT', + body: { region: region ?? '', ...body }, + }) +} + +export function terminateInstance( + cfgId: number, + instanceId: string, + preserveBootVolume: boolean, + region?: string, +): Promise { + if (mockOn) return mocked(undefined, 600) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}`, { + method: 'DELETE', + query: { preserveBootVolume, region }, + }) +} + +export function instanceAction( + cfgId: number, + instanceId: string, + action: PowerAction, + region?: string, +): Promise { + if (mockOn) { + const next = action === 'STOP' || action === 'SOFTSTOP' ? 'STOPPING' : 'STARTING' + return mocked({ ...mockInstances[1][0], lifecycleState: next }, 600) + } + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/action`, { + method: 'POST', + body: { region: region ?? '', action }, + }) +} + +export function changePublicIp( + cfgId: number, + instanceId: string, + region?: string, +): Promise<{ publicIp: string }> { + if (mockOn) return mocked({ publicIp: '130.61.99.201' }, 900) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/change-public-ip`, + { method: 'POST', body: { region: region ?? '' } }, + ) +} + +export function addIpv6( + cfgId: number, + instanceId: string, + address = '', + region?: string, +): Promise<{ ipv6Address: string }> { + if (mockOn) return mocked({ ipv6Address: address || '2603:c020:400d:aa01::2' }, 700) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/ipv6-addresses`, { + method: 'POST', + body: { region: region ?? '', address }, + }) +} + +export function removeIpv6( + cfgId: number, + instanceId: string, + address: string, + region?: string, +): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/ipv6-addresses`, { + method: 'DELETE', + query: { address, region }, + }) +} + +// ---- VNIC 管理 ---- +export function listVnics(cfgId: number, instanceId: string, region?: string): Promise { + if (mockOn) return mocked([]) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, { + query: { region }, + }) +} + +export function attachVnic( + cfgId: number, + instanceId: string, + body: AttachVnicRequest, +): Promise { + if (mockOn) return Promise.reject(new Error('mock 环境不支持附加 VNIC')) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/vnics`, { + method: 'POST', + body: { ...body, region: body.region ?? '' }, + }) +} + +export function detachVnic(cfgId: number, attachmentId: string, region?: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/vnic-attachments/${encodeURIComponent(attachmentId)}`, { + method: 'DELETE', + query: { region }, + }) +} + +/** 为指定 VNIC 添加 IPv6;address 留空自动分配。删除复用实例级 removeIpv6(按地址遍历全部网卡) */ +export function addVnicIpv6( + cfgId: number, + vnicId: string, + address = '', + region?: string, +): Promise<{ address: string }> { + if (mockOn) return mocked({ address: address || '2603:c020:400d:aa01::3' }, 700) + return request(`/oci-configs/${cfgId}/vnics/${encodeURIComponent(vnicId)}/ipv6-addresses`, { + method: 'POST', + body: { region: region ?? '', address }, + }) +} + +// ---- 网页控制台(串行 / VNC)---- +export type ConsoleSessionType = 'serial' | 'vnc' + +export function createConsoleSession( + cfgId: number, + instanceId: string, + type: ConsoleSessionType, + region?: string, +): Promise<{ sessionId: string; type: ConsoleSessionType }> { + if (mockOn) return Promise.reject(new Error('mock 环境不支持网页控制台')) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-sessions`, + { method: 'POST', body: { type, region: region ?? '' } }, + ) +} + +export function deleteConsoleSession(sessionId: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/console-sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }) +} + +/** 控制台数据面 WS 地址:浏览器 WS 无法带自定义头,token 走 query */ +export function consoleSessionWsUrl(sessionId: string, token: string, extra?: string): string { + const proto = location.protocol === 'https:' ? 'wss' : 'ws' + const q = extra ? `&${extra}` : '' + return `${proto}://${location.host}/api/v1/console-sessions/${encodeURIComponent(sessionId)}/ws?token=${encodeURIComponent(token)}${q}` +} + +// ---- 控制台连接 ---- +export function listConsoleConnections( + cfgId: number, + instanceId: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockConsoleConns) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-connections`, + { query: { region } }, + ) +} + +export function createConsoleConnection( + cfgId: number, + instanceId: string, + sshPublicKey: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockConsoleConns[0], 1500) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/console-connections`, + { method: 'POST', body: { region: region ?? '', sshPublicKey } }, + ) +} + +export function deleteConsoleConnection( + cfgId: number, + connectionId: string, + region?: string, +): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/console-connections/${encodeURIComponent(connectionId)}`, { + method: 'DELETE', + query: { region }, + }) +} + +// ---- 引导卷挂载 ---- +export function listBootVolumeAttachments( + cfgId: number, + instanceId: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockBvAttachments) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/boot-volume-attachments`, + { query: { region } }, + ) +} + +export function attachBootVolume( + cfgId: number, + instanceId: string, + bootVolumeId: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockBvAttachments[0], 1200) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/boot-volume-attachments`, + { method: 'POST', body: { region: region ?? '', bootVolumeId } }, + ) +} + +export function replaceBootVolume( + cfgId: number, + instanceId: string, + bootVolumeId: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockBvAttachments[0], 1500) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/replace-boot-volume`, + { method: 'POST', body: { region: region ?? '', bootVolumeId } }, + ) +} + +export function detachBootVolume( + cfgId: number, + attachmentId: string, + region?: string, +): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/boot-volume-attachments/${encodeURIComponent(attachmentId)}`, { + method: 'DELETE', + query: { region }, + }) +} + +// ---- 块卷挂载 ---- +export function listVolumeAttachments( + cfgId: number, + instanceId: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockVolAttachments) + return request( + `/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/volume-attachments`, + { query: { region } }, + ) +} + +export function attachVolume( + cfgId: number, + instanceId: string, + volumeId: string, + readOnly = false, + region?: string, +): Promise { + if (mockOn) return mocked(mockVolAttachments[0], 1000) + return request(`/oci-configs/${cfgId}/instances/${encodeURIComponent(instanceId)}/volume-attachments`, { + method: 'POST', + body: { region: region ?? '', volumeId, readOnly }, + }) +} + +export function detachVolume(cfgId: number, attachmentId: string, region?: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/volume-attachments/${encodeURIComponent(attachmentId)}`, { + method: 'DELETE', + query: { region }, + }) +} diff --git a/src/api/logevents.ts b/src/api/logevents.ts new file mode 100644 index 0000000..e07d61a --- /dev/null +++ b/src/api/logevents.ts @@ -0,0 +1,104 @@ +import { mockLogEvents, mockLogRelays, mockLogWebhooks } from './mock' +import { mockOn, mocked, request } from './request' +import type { LogEventPage, LogRelayView, LogWebhookInfo, LogWebhookState } from '@/types/api' + +export interface LogEventParams { + page: number + pageSize: number + /** 按租户过滤,缺省为全部 */ + cfgId?: number +} + +export function listLogEvents(params: LogEventParams): Promise { + if (mockOn) { + const filtered = params.cfgId + ? mockLogEvents.filter((e) => e.ociConfigId === params.cfgId) + : mockLogEvents + const start = (params.page - 1) * params.pageSize + return mocked({ + items: filtered.slice(start, start + params.pageSize), + total: filtered.length, + }) + } + return request('/log-events', { query: { ...params } }) +} + +export function getLogWebhook(cfgId: number): Promise { + if (mockOn) { + const info = mockLogWebhooks.get(cfgId) + return mocked(info ? { exists: true, webhook: { ...info } } : { exists: false }) + } + return request(`/oci-configs/${cfgId}/log-webhook`) +} + +/** 生成(或幂等返回)回传回调地址 */ +export function ensureLogWebhook(cfgId: number): Promise { + if (mockOn) { + let info = mockLogWebhooks.get(cfgId) + if (!info) { + const secret = `mock${cfgId}`.padEnd(64, '0') + info = { + path: `/api/v1/webhooks/oci-logs/${secret}`, + secret, + createdAt: new Date().toISOString(), + } + mockLogWebhooks.set(cfgId, info) + } + return mocked({ ...info }, 400) + } + return request(`/oci-configs/${cfgId}/log-webhook`, { method: 'POST' }) +} + +/** 撤销回调地址,旧 URL 随即 404 */ +export function revokeLogWebhook(cfgId: number): Promise { + if (mockOn) { + mockLogWebhooks.delete(cfgId) + return mocked(undefined, 300) + } + return request(`/oci-configs/${cfgId}/log-webhook`, { method: 'DELETE' }) +} + +/** 查询 OCI 侧链路状态与关键事件清单 */ +export function getLogRelay(cfgId: number): Promise { + if (mockOn) return mocked(structuredCloneRelay(cfgId)) + return request(`/oci-configs/${cfgId}/log-relay`) +} + +/** 一键建立回传链路(P1):同步执行,含订阅确认与 Connector 就绪等待,可达 2 分钟 */ +export function setupLogRelay(cfgId: number): Promise { + if (mockOn) { + mockLogRelays.set(cfgId, true) + return mocked(structuredCloneRelay(cfgId), 1500) + } + return request(`/oci-configs/${cfgId}/log-relay`, { method: 'POST' }) +} + +/** 销毁 OCI 侧链路并撤销回调地址 */ +export function teardownLogRelay(cfgId: number): Promise { + if (mockOn) { + mockLogRelays.delete(cfgId) + mockLogWebhooks.delete(cfgId) + return mocked(undefined, 800) + } + return request(`/oci-configs/${cfgId}/log-relay`, { method: 'DELETE' }) +} + +/** mock 模式下按已建状态拼一份链路视图 */ +function structuredCloneRelay(cfgId: number): LogRelayView { + const built = mockLogRelays.get(cfgId) === true + const res = (suffix: string) => (built ? { id: `ocid1.mock.${suffix}`, state: 'ACTIVE' } : { id: '', state: '' }) + return { + endpoint: built ? `https://demo.example.com/api/v1/webhooks/oci-logs/mock${cfgId}` : undefined, + topic: res('topic'), + subscription: res('sub'), + connector: res('conn'), + policy: res('policy'), + ready: built, + events: [ + 'LaunchInstance', 'TerminateInstance', 'InstanceAction', + 'CreateUser', 'DeleteUser', 'UpdateUser', + 'CreateApiKey', 'DeleteApiKey', 'UpdateUserCapabilities', + 'CreateRegionSubscription', 'CreatePolicy', 'UpdatePolicy', 'DeletePolicy', + ], + } +} diff --git a/src/api/mock.ts b/src/api/mock.ts new file mode 100644 index 0000000..d25d820 --- /dev/null +++ b/src/api/mock.ts @@ -0,0 +1,1716 @@ +import type { + SecuritySetting, + AuditEvent, + BootVolume, + BootVolumeAttachment, + Compartment, + ComputeShape, + ConsoleConnection, + CostItem, + IamUser, + IamUserDetail, + IdentityProvider, + IdentitySetting, + ImageInfo, + Instance, + InstanceTraffic, + Ipv6Report, + LimitItem, + LogEvent, + LogWebhookInfo, + NotificationRecipients, + NotifyEventsSetting, + OciConfig, + Overview, + PasswordPolicy, + RegionInfo, + RegionSubscription, + SecurityList, + SignOnRule, + Subnet, + SubscriptionDetail, + SystemLog, + Task, + TaskLog, + TaskSetting, + TelegramSetting, + Vcn, + Volume, + VolumeAttachment, +} from '@/types/api' + +/** mock 数据集:与设计稿展示数据保持一致,便于对照验收 */ + +function cfg(p: Partial & Pick): OciConfig { + return { + group: '', + userOcid: 'ocid1.user.oc1..aaaa', + tenancyOcid: `ocid1.tenancy.oc1..${p.alias}xxxx`, + fingerprint: 'e2:d4:00:11:22:33', + region: 'eu-frankfurt-1', + tenancyName: p.alias ?? '', + homeRegionKey: 'FRA', + accountType: 'free', + subscriptionId: 'ocid1.organizationssubscription.oc1..xxx', + paymentModel: 'FREE_TRIAL', + subscriptionTier: 'FREE', + promotionStatus: 'EXPIRED', + promotionAmount: 279, + promotionExpires: '2026-02-21T00:00:00Z', + aliveStatus: 'alive', + lastError: '', + lastVerifiedAt: '2026-07-03T15:40:00+08:00', + multiRegion: false, + multiCompartment: false, + proxyId: null, + proxyName: '', + createdAt: '2026-05-14T10:00:00+08:00', + updatedAt: '2026-07-03T15:40:00+08:00', + ...p, + } +} + +export const mockProxies = [ + { + id: 1, + name: '东京 socks5', + type: 'socks5', + host: '203.0.113.50', + port: 1080, + username: 'relay', + passwordSet: true, + country: '日本', + city: '东京', + geoAt: '2026-07-05T12:00:00+08:00', + usedBy: 1, + createdAt: '2026-07-01T10:00:00+08:00', + }, + { + id: 2, + name: '本地 http', + type: 'http', + host: '127.0.0.1', + port: 7890, + username: '', + passwordSet: false, + country: '', + city: '', + geoAt: '2026-07-05T12:00:00+08:00', + usedBy: 0, + createdAt: '2026-07-03T09:30:00+08:00', + }, +] + +export const mockConfigs: OciConfig[] = [ + cfg({ + id: 1, + alias: 'BCDE主号', + group: '生产', + tenancyName: 'bcdemain', + accountType: 'paid', + paymentModel: 'PAYG', + subscriptionTier: 'FREE_AND_PAID', + promotionStatus: 'EXPIRED', + multiRegion: true, + multiCompartment: true, + proxyId: null, + proxyName: '', + }), + cfg({ + id: 2, + alias: '试用期', + group: '教育', + tenancyName: 'trialacct', + region: 'us-ashburn-1', + homeRegionKey: 'IAD', + accountType: 'trial', + subscriptionTier: 'FREE_AND_TRIAL', + promotionStatus: 'ACTIVE', + promotionExpires: '2026-07-31T00:00:00Z', + }), + cfg({ + id: 3, + alias: '新加坡备用', + tenancyName: 'sgbackup', + region: 'ap-singapore-1', + homeRegionKey: 'SIN', + }), + cfg({ + id: 4, + alias: '首尔小鸡', + group: '教育', + tenancyName: 'seoulchick', + region: 'ap-seoul-1', + homeRegionKey: 'ICN', + accountType: 'trial', + promotionStatus: 'ACTIVE', + promotionExpires: '2026-07-18T00:00:00Z', + }), + cfg({ + id: 5, + alias: '东京主号', + tenancyName: 'tokyomain', + region: 'ap-tokyo-1', + homeRegionKey: 'NRT', + aliveStatus: 'dead', + lastError: 'NotAuthenticated: key expired', + lastVerifiedAt: '2026-07-03T13:40:00+08:00', + }), + cfg({ + id: 6, + alias: '伦敦过期', + tenancyName: 'londonold', + region: 'uk-london-1', + homeRegionKey: 'LHR', + aliveStatus: 'dead', + lastError: 'NotAuthenticated: tenancy suspended', + lastVerifiedAt: '2026-07-02T15:00:00+08:00', + }), + cfg({ id: 7, alias: '抢机备号', tenancyName: 'snatchbak' }), +] + +function inst(p: Partial & Pick): Instance { + return { + lifecycleState: 'RUNNING', + shape: 'VM.Standard.A1.Flex', + ocpus: 4, + memoryInGBs: 24, + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + region: 'eu-frankfurt-1', + compartmentId: 'ocid1.compartment.oc1..root', + imageId: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2404', + imageName: 'Canonical-Ubuntu-24.04-aarch64-2026.05.15-0', + subnetId: 'ocid1.subnet.oc1.eu-frankfurt-1.pub', + privateIp: '10.0.1.2', + publicIp: '130.61.88.147', + ipv6Addresses: ['2603:c020:400d:aa01::1'], + freeformTags: { env: 'prod' }, + definedTags: { + 'Oracle-Tags': { CreatedBy: 'default/admin', CreatedOn: '2026-05-20T14:32:00.000Z' }, + }, + timeCreated: '2026-05-20T14:32:00Z', + ...p, + } +} + +/** 按配置 id 组织的实例 mock */ +export const mockInstances: Record = { + 1: [ + inst({ + id: 'ocid1.instance.oc1..web-fra-01', + displayName: 'web-fra-01', + freeformTags: { env: 'prod', RootPassword: 'n^m4#wvErNeESq2K' }, + }), + inst({ + id: 'ocid1.instance.oc1..web-fra-02', + displayName: 'web-fra-02', + ocpus: 2, + memoryInGBs: 12, + publicIp: '130.61.92.33', + privateIp: '10.0.1.3', + ipv6Addresses: [], + }), + inst({ + id: 'ocid1.instance.oc1..app-fra-03', + displayName: 'app-fra-03', + ocpus: 2, + memoryInGBs: 12, + publicIp: '141.147.62.190', + privateIp: '10.0.1.4', + ipv6Addresses: [], + }), + ], + 2: [ + inst({ + id: 'ocid1.instance.oc1..dev-ash-01', + displayName: 'dev-ash-01', + shape: 'VM.Standard.E2.1.Micro', + ocpus: 1, + memoryInGBs: 1, + region: 'us-ashburn-1', + availabilityDomain: 'RssO:US-ASHBURN-1-AD-2', + publicIp: '129.153.44.92', + ipv6Addresses: [], + }), + ], + 3: [ + inst({ + id: 'ocid1.instance.oc1..db-sgp-01', + displayName: 'db-sgp-01', + shape: 'VM.Standard.E4.Flex', + ocpus: 2, + memoryInGBs: 16, + region: 'ap-singapore-1', + availabilityDomain: 'RssO:AP-SINGAPORE-1-AD-1', + publicIp: '152.69.204.18', + ipv6Addresses: [], + }), + ], + 4: [ + inst({ + id: 'ocid1.instance.oc1..proxy-icn-01', + displayName: 'proxy-icn-01', + shape: 'VM.Standard.E2.1.Micro', + ocpus: 1, + memoryInGBs: 1, + lifecycleState: 'STOPPED', + region: 'ap-seoul-1', + availabilityDomain: 'RssO:AP-SEOUL-1-AD-1', + publicIp: '144.24.71.206', + ipv6Addresses: [], + }), + ], + 5: [ + inst({ + id: 'ocid1.instance.oc1..backup-nrt-01', + displayName: 'backup-nrt-01', + ocpus: 1, + memoryInGBs: 6, + lifecycleState: 'STOPPED', + region: 'ap-tokyo-1', + availabilityDomain: 'RssO:AP-TOKYO-1-AD-1', + publicIp: '140.238.51.77', + ipv6Addresses: [], + }), + ], + 6: [], + 7: [ + inst({ + id: 'ocid1.instance.oc1..snatch-fra-a1', + displayName: 'snatch-fra-a1', + lifecycleState: 'PROVISIONING', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-2', + publicIp: '', + ipv6Addresses: [], + }), + ], +} + +export const mockTasks: Task[] = [ + { + id: 1, + name: '每日全量测活', + type: 'health_check', + cronExpr: '0 3 * * *', + payload: '{"ociConfigIds":[]}', + status: 'active', + lastRunAt: '2026-07-03T03:00:00+08:00', + lastError: '', + runCount: 46, + createdAt: '2026-05-18T00:00:00+08:00', + updatedAt: '2026-07-03T03:00:00+08:00', + }, + { + id: 6, + name: '每日成本同步', + type: 'cost', + cronExpr: '30 3 * * *', + payload: '{"ociConfigIds":[]}', + status: 'active', + lastRunAt: '2026-07-03T03:30:00+08:00', + lastError: '', + runCount: 21, + createdAt: '2026-06-12T00:00:00+08:00', + updatedAt: '2026-07-03T03:30:00+08:00', + }, + { + id: 2, + name: '法兰克福 A1 抢机 ×2', + type: 'snatch', + cronExpr: '*/5 * * * *', + payload: '{"ociConfigId":7,"count":2}', + status: 'active', + lastRunAt: '2026-07-03T17:02:00+08:00', + lastError: 'Out of host capacity (AD-1)', + runCount: 217, + createdAt: '2026-06-20T00:00:00+08:00', + updatedAt: '2026-07-03T17:02:00+08:00', + }, + { + id: 3, + name: '新加坡 A1 抢机', + type: 'snatch', + cronExpr: '*/10 * * * *', + payload: '{"ociConfigId":3,"count":1}', + status: 'paused', + lastRunAt: '2026-07-02T22:40:00+08:00', + lastError: '', + runCount: 89, + createdAt: '2026-06-25T00:00:00+08:00', + updatedAt: '2026-07-02T22:40:00+08:00', + }, + { + id: 4, + name: '首尔 E2 抢机', + type: 'snatch', + cronExpr: '*/5 * * * *', + payload: '{"ociConfigId":4,"count":1}', + status: 'succeeded', + lastRunAt: '2026-07-01T14:25:00+08:00', + lastError: '', + runCount: 132, + createdAt: '2026-06-28T00:00:00+08:00', + updatedAt: '2026-07-01T14:25:00+08:00', + }, + { + id: 7, + name: '东京 A1 抢机', + type: 'snatch', + cronExpr: '*/5 * * * *', + payload: '{"ociConfigId":5,"count":1,"totalCount":1,"authFailCount":3}', + status: 'failed', + lastRunAt: '2026-07-03T14:10:00+08:00', + lastError: '连续 3 次 NotAuthenticated,任务已熔断停止: NotAuthenticated: key expired', + runCount: 58, + createdAt: '2026-06-30T00:00:00+08:00', + updatedAt: '2026-07-03T14:10:00+08:00', + }, + { + id: 5, + name: 'BCDE 每小时测活', + type: 'health_check', + cronExpr: '7 * * * *', + payload: '{"ociConfigIds":[1,2,3,4]}', + status: 'active', + lastRunAt: '2026-07-03T17:07:00+08:00', + lastError: '', + runCount: 312, + createdAt: '2026-06-01T00:00:00+08:00', + updatedAt: '2026-07-03T17:07:00+08:00', + }, +] + +export const mockTaskLogs: Record = { + 1: [ + { + id: 101, + taskId: 1, + success: true, + message: 'checked 21: 21 alive, 0 dead', + durationMs: 3912, + createdAt: '2026-07-03T03:00:04+08:00', + }, + ], + 6: [ + { + id: 601, + taskId: 6, + success: true, + message: 'synced usage for 3 tenants, skipped 4 free', + durationMs: 5210, + createdAt: '2026-07-03T03:30:05+08:00', + }, + ], + 2: [ + { + id: 201, + taskId: 2, + success: false, + message: 'Out of host capacity (AD-1)', + durationMs: 1204, + createdAt: '2026-07-03T17:02:01+08:00', + }, + { + id: 200, + taskId: 2, + success: false, + message: 'Out of host capacity (AD-1)', + durationMs: 987, + createdAt: '2026-07-03T16:57:01+08:00', + }, + { + id: 199, + taskId: 2, + success: false, + message: 'Out of host capacity (AD-2)', + durationMs: 1102, + createdAt: '2026-07-03T16:52:01+08:00', + }, + { + id: 180, + taskId: 2, + success: true, + message: 'launched 1/2: ocid1.instance.oc1.eu-frankfurt-1.antheljt…mc25a, 1 remaining', + durationMs: 42381, + createdAt: '2026-07-02T09:15:00+08:00', + }, + ], + 4: [ + { + id: 401, + taskId: 4, + success: true, + message: 'launched ocid1.instance.oc1.ap-seoul-1.anuwg…7k2ma, task succeeded', + durationMs: 38754, + createdAt: '2026-07-01T14:25:39+08:00', + }, + ], + 7: [ + { + id: 703, + taskId: 7, + success: false, + message: '连续 3 次 NotAuthenticated,任务已熔断停止: NotAuthenticated: key expired', + durationMs: 842, + createdAt: '2026-07-03T14:10:01+08:00', + }, + { + id: 702, + taskId: 7, + success: false, + message: 'no instance created: vm: NotAuthenticated: key expired', + durationMs: 913, + createdAt: '2026-07-03T14:05:01+08:00', + }, + { + id: 701, + taskId: 7, + success: false, + message: 'no instance created: vm: NotAuthenticated: key expired', + durationMs: 887, + createdAt: '2026-07-03T14:00:01+08:00', + }, + ], + 5: [ + { + id: 501, + taskId: 5, + success: true, + message: 'checked 4: 4 alive, 0 dead', + durationMs: 1533, + createdAt: '2026-07-03T17:07:02+08:00', + }, + ], +} + +export const mockRegionSubs: RegionSubscription[] = [ + { + key: 'FRA', + name: 'eu-frankfurt-1', + status: 'READY', + isHomeRegion: true, + alias: 'Germany Central (Frankfurt)', + }, + { + key: 'AMS', + name: 'eu-amsterdam-1', + status: 'READY', + isHomeRegion: false, + alias: 'Netherlands NW (Amsterdam)', + }, + { + key: 'ICN', + name: 'ap-seoul-1', + status: 'IN_PROGRESS', + isHomeRegion: false, + alias: 'South Korea Central (Seoul)', + }, +] + +export const mockLimits: LimitItem[] = [ + { + name: 'standard-a1-core-count', + scopeType: 'AD', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + value: 4, + used: 4, + available: 0, + }, + { + name: 'standard-a1-memory-count', + scopeType: 'AD', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + value: 24, + used: 24, + available: 0, + }, + { + name: 'standard-e2-micro-core-count', + scopeType: 'AD', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + value: 2, + used: 1, + available: 1, + }, + { + name: 'standard-e4-core-count', + scopeType: 'AD', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + value: 6, + used: 2, + available: 4, + }, + { name: 'vcn-count', scopeType: 'REGION', value: 50, used: 2, available: 48 }, + { name: 'volume-backup-count', scopeType: 'REGION', value: 100, used: 6, available: 94 }, +] + +export const mockSubscriptions: SubscriptionDetail[] = [ + { + id: 'ocid1.organizationssubscription.oc1..xxx', + serviceName: 'CLOUDCM', + classicSubscriptionId: 'xxx', + paymentModel: 'FREE_TRIAL', + subscriptionTier: 'FREE_AND_TRIAL', + lifecycleState: 'ACTIVE', + programType: '', + customerCountryCode: 'DE', + cloudAmountCurrency: 'EUR', + csiNumber: '', + regionAssignment: '', + isGovernmentSubscription: false, + promotions: [ + { + duration: 30, + durationUnit: 'DAYS', + amount: 279, + status: 'ACTIVE', + isIntentToPay: false, + currencyUnit: 'EUR', + timeStarted: '2026-01-22T01:19:25Z', + timeExpired: '2026-07-31T00:00:00Z', + }, + ], + startDate: '2026-01-22T01:19:25Z', + endDate: null, + timeCreated: '2026-01-22T01:19:25.174Z', + timeUpdated: '2026-01-22T01:35:51.021Z', + }, + { + id: 'ocid1.organizationssubscription.oc1..yyy', + serviceName: 'OCICORP', + classicSubscriptionId: 'yyy', + paymentModel: 'PAYG', + subscriptionTier: 'FREE_AND_PAID', + lifecycleState: 'ACTIVE', + programType: 'STANDARD', + customerCountryCode: 'DE', + cloudAmountCurrency: 'EUR', + csiNumber: '', + regionAssignment: '', + isGovernmentSubscription: false, + promotions: [], + startDate: '2026-03-02T00:00:00Z', + endDate: null, + timeCreated: '2026-03-02T00:00:00.000Z', + timeUpdated: '2026-03-02T00:00:00.000Z', + }, + { + id: 'ocid1.organizationssubscription.oc1..idcs', + serviceName: 'IDCS', + classicSubscriptionId: 'zzz', + paymentModel: '', + subscriptionTier: '', + lifecycleState: 'ACTIVE', + programType: '', + customerCountryCode: 'DE', + cloudAmountCurrency: '', + csiNumber: '', + regionAssignment: '', + isGovernmentSubscription: false, + promotions: [], + startDate: '2026-01-22T01:19:25Z', + endDate: null, + timeCreated: '2026-01-22T01:19:25.174Z', + timeUpdated: '2026-01-22T01:19:25.174Z', + }, +] + +export const mockUsers: IamUser[] = [ + { + id: 'ocid1.user..apiadmin', + name: 'api-admin', + description: '当前 API Key 签名用户', + email: 'admin@bcde.io', + emailVerified: true, + mfaActivated: true, + lifecycleState: 'ACTIVE', + isCurrentUser: true, + timeCreated: '2026-05-14T00:00:00Z', + lastLoginTime: '2026-07-03T09:00:00Z', + }, + { + id: 'ocid1.user..consoleadmin', + name: 'console-admin', + description: '控制台管理员', + email: 'console-admin@bcde.io', + emailVerified: true, + mfaActivated: true, + lifecycleState: 'ACTIVE', + isCurrentUser: false, + timeCreated: '2026-05-20T00:00:00Z', + lastLoginTime: null, + }, + { + id: 'ocid1.user..devzhang', + name: 'dev-zhang', + description: '开发', + email: 'zhang@bcde.io', + emailVerified: false, + mfaActivated: false, + lifecycleState: 'ACTIVE', + isCurrentUser: false, + timeCreated: '2026-06-02T00:00:00Z', + lastLoginTime: null, + }, + { + id: 'ocid1.user..fedwang', + name: 'bcde_fed/wang.defa', + description: '联邦用户 · Keycloak', + email: '', + emailVerified: false, + mfaActivated: false, + lifecycleState: 'ACTIVE', + isCurrentUser: false, + timeCreated: '2026-06-11T00:00:00Z', + lastLoginTime: '2026-07-02T20:00:00Z', + }, +] + +// 域档案:apiadmin 双管理员,consoleadmin 仅管理员组,devzhang 普通域用户, +// fedwang 不在身份域中(演示经典 IAM 用户的禁用态) +export const mockUserDetails: Record = { + 'ocid1.user..apiadmin': { + inDomain: true, + givenName: 'API', + familyName: 'Admin', + isDomainAdmin: true, + inAdminGroup: true, + }, + 'ocid1.user..consoleadmin': { + inDomain: true, + givenName: 'Console', + familyName: 'Admin', + isDomainAdmin: false, + inAdminGroup: true, + }, + 'ocid1.user..devzhang': { + inDomain: true, + givenName: 'Zhang', + familyName: 'Dev', + isDomainAdmin: false, + inAdminGroup: false, + }, +} + +// 审计事件按时间倒序,含 401/404 失败事件(状态 >=400 标红演示); +// eventId 统一后缀派生,详情接口按它反查 +const mockAuditEventsBase: Omit[] = [ + { + eventTime: '2026-07-07T09:42:00Z', + eventName: 'TerminateInstance', + source: 'ComputeApi', + resourceName: 'web-1', + compartmentName: 'bcde (root)', + principalName: 'api-admin', + ipAddress: '203.0.113.10', + status: '204', + requestAction: 'DELETE', + requestPath: '/20160918/instances/ocid1.instance.oc1.eu-frankfurt-1.aaaa', + }, + { + eventTime: '2026-07-07T09:40:00Z', + eventName: 'LaunchInstance', + source: 'ComputeApi', + resourceName: 'arm-snatch-1', + compartmentName: 'bcde (root)', + principalName: 'api-admin', + ipAddress: '203.0.113.10', + status: '500', + requestAction: 'POST', + requestPath: '/20160918/instances', + }, + { + eventTime: '2026-07-07T08:15:00Z', + eventName: 'UpdateVcn', + source: 'VcnApi', + resourceName: 'vcn-20260509-0252', + compartmentName: 'network', + principalName: 'console-admin', + ipAddress: '198.51.100.7', + status: '200', + requestAction: 'PUT', + requestPath: '/20160918/vcns/ocid1.vcn.oc1.eu-frankfurt-1.bbbb', + }, + { + eventTime: '2026-07-07T06:30:00Z', + eventName: 'GetUser', + source: 'IdentityControlPlane', + resourceName: 'dev-zhang', + compartmentName: 'bcde (root)', + principalName: 'dev-zhang', + ipAddress: '198.51.100.24', + status: '404', + requestAction: 'GET', + requestPath: '/20160918/users/ocid1.user.oc1..missing', + }, + { + eventTime: '2026-07-07T03:12:00Z', + eventName: 'InstanceAction', + source: 'ComputeApi', + resourceName: 'db-1', + compartmentName: 'prod', + principalName: 'api-admin', + ipAddress: '203.0.113.10', + status: '200', + requestAction: 'POST', + requestPath: '/20160918/instances/ocid1.instance.oc1.eu-frankfurt-1.cccc', + }, + { + eventTime: '2026-07-06T22:47:00Z', + eventName: 'ListEvents', + source: 'Audit', + resourceName: '', + compartmentName: 'bcde (root)', + principalName: 'console-admin', + ipAddress: '198.51.100.7', + status: '200', + requestAction: 'GET', + requestPath: '/20190901/auditEvents', + }, + { + eventTime: '2026-07-06T20:05:00Z', + eventName: 'CreateSwiftPassword', + source: 'IdentityControlPlane', + resourceName: 'dev-zhang', + compartmentName: 'bcde (root)', + principalName: 'dev-zhang', + ipAddress: '198.51.100.24', + status: '401', + requestAction: 'POST', + requestPath: '/20160918/users/ocid1.user..devzhang/swiftPasswords', + }, + { + eventTime: '2026-07-06T15:58:00Z', + eventName: 'UpdateSecurityList', + source: 'VcnApi', + resourceName: 'Default Security List', + compartmentName: 'network', + principalName: 'bcde_fed/wang.defa', + ipAddress: '192.0.2.88', + status: '200', + requestAction: 'PUT', + requestPath: '/20160918/securityLists/ocid1.securitylist.oc1.eu-frankfurt-1.dddd', + }, + { + eventTime: '2026-07-06T12:00:00Z', + eventName: '', + source: 'CloudGuardResponderEngine', + resourceName: '', + compartmentName: 'bcde (root)', + principalName: '', + ipAddress: '', + status: '', + requestAction: '', + requestPath: '', + }, +] + +export const mockAuditEvents: AuditEvent[] = mockAuditEventsBase.map((e, i) => ({ + ...e, + eventId: `mock-audit-evt-${i + 1}`, +})) + +export const mockIdps: IdentityProvider[] = [ + { + id: '54a0260f7a75', + name: 'BCDE-keycloak', + type: 'SAML', + enabled: true, + partnerProviderId: 'https://auth.example.test/realms/oci', + jitEnabled: true, + timeCreated: '2026-06-10T00:00:00Z', + }, + { + id: '8800aa11bb22', + name: 'test-okta', + type: 'SAML', + enabled: false, + partnerProviderId: 'https://test.okta.example/app/xyz', + jitEnabled: true, + timeCreated: '2026-06-15T00:00:00Z', + }, +] + +export const mockSignOnRules: SignOnRule[] = [ + { + id: '873aa3fa1a72', + name: 'skip-mfa-keycloak', + sequence: 1, + authenticationFactor: 'IDP', + conditionAttribute: 'subject.authenticatedBy', + conditionValue: '["54a0260f7a75"]', + builtIn: false, + }, + { + id: 'builtin-default', + name: 'Default Rule', + sequence: 2, + authenticationFactor: 'IDP_PLUS_2FA', + conditionAttribute: '', + conditionValue: '', + builtIn: true, + }, +] + +export const mockPolicies: PasswordPolicy[] = [ + { + id: 'defaultPasswordPolicy', + name: 'defaultPasswordPolicy', + priority: 3, + passwordStrength: 'Custom', + passwordExpiresAfter: 120, + minLength: 12, + numPasswordsInHistory: 4, + }, + { + id: 'standardPasswordPolicy', + name: 'StandardPasswordPolicy', + priority: 2, + passwordStrength: 'Standard', + passwordExpiresAfter: 0, + minLength: 8, + }, + { + id: 'simplePasswordPolicy', + name: 'SimplePasswordPolicy', + priority: 1, + passwordStrength: 'Simple', + passwordExpiresAfter: 0, + minLength: 8, + }, +] + +export const mockRecipients: NotificationRecipients = { + recipients: ['admin@bcde.io', 'ops@bcde.io'], + testModeEnabled: true, +} + +export const mockIdentitySetting: IdentitySetting = { + id: 'IdentitySettings', + primaryEmailRequired: false, +} + +export const mockCosts: CostItem[] = [ + { + timeStart: '2026-06-27T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 5.2, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-06-28T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 5.8, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-06-29T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 5.4, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-06-30T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 6.1, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-07-01T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 6.6, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-07-02T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 6.3, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-07-03T00:00:00Z', + groupValue: 'COMPUTE', + computedAmount: 6.8, + computedQuantity: 96, + currency: 'USD', + unit: 'OCPU_HOURS', + }, + { + timeStart: '2026-07-01T00:00:00Z', + groupValue: 'BLOCK_STORAGE', + computedAmount: 1.4, + computedQuantity: 300, + currency: 'USD', + unit: 'GB_MONTHS', + }, + { + timeStart: '2026-07-02T00:00:00Z', + groupValue: 'BLOCK_STORAGE', + computedAmount: 1.4, + computedQuantity: 300, + currency: 'USD', + unit: 'GB_MONTHS', + }, +] + +export const mockTraffic: InstanceTraffic = { + instanceId: 'ocid1.instance.oc1..web-fra-01', + inboundBytes: 96.2 * 1024 ** 3, + outboundBytes: 128.4 * 1024 ** 3, + vnics: [ + { + vnicId: 'ocid1.vnic..main', + inbound: [ + { timestamp: '2026-06-27T00:00:00Z', bytes: 12.1 * 1024 ** 3 }, + { timestamp: '2026-06-28T00:00:00Z', bytes: 13.8 * 1024 ** 3 }, + { timestamp: '2026-06-29T00:00:00Z', bytes: 12.9 * 1024 ** 3 }, + { timestamp: '2026-06-30T00:00:00Z', bytes: 14.5 * 1024 ** 3 }, + { timestamp: '2026-07-01T00:00:00Z', bytes: 15.2 * 1024 ** 3 }, + { timestamp: '2026-07-02T00:00:00Z', bytes: 13.4 * 1024 ** 3 }, + { timestamp: '2026-07-03T00:00:00Z', bytes: 14.3 * 1024 ** 3 }, + ], + outbound: [ + { timestamp: '2026-06-27T00:00:00Z', bytes: 16.2 * 1024 ** 3 }, + { timestamp: '2026-06-28T00:00:00Z', bytes: 18.5 * 1024 ** 3 }, + { timestamp: '2026-06-29T00:00:00Z', bytes: 17.1 * 1024 ** 3 }, + { timestamp: '2026-06-30T00:00:00Z', bytes: 19.8 * 1024 ** 3 }, + { timestamp: '2026-07-01T00:00:00Z', bytes: 21.5 * 1024 ** 3 }, + { timestamp: '2026-07-02T00:00:00Z', bytes: 17.9 * 1024 ** 3 }, + { timestamp: '2026-07-03T00:00:00Z', bytes: 17.4 * 1024 ** 3 }, + ], + }, + ], +} + +export const mockVcns: Record = { + 1: [ + { + id: 'ocid1.vcn.oc1..autovcn', + displayName: 'oci-portal-auto-vcn', + lifecycleState: 'AVAILABLE', + compartmentId: 'ocid1.compartment.oc1..root', + cidrBlocks: ['10.0.0.0/16'], + ipv6CidrBlocks: ['2603:c020:8026:9c00::/56'], + dnsLabel: 'ociportal', + defaultRouteTableId: 'ocid1.routetable..default', + defaultSecurityListId: 'ocid1.securitylist..default', + timeCreated: '2026-05-20T14:30:00Z', + }, + { + id: 'ocid1.vcn.oc1..webvcn', + displayName: 'web-prod-vcn', + lifecycleState: 'AVAILABLE', + compartmentId: 'ocid1.compartment.oc1..root', + cidrBlocks: ['10.10.0.0/16'], + ipv6CidrBlocks: [], + dnsLabel: 'webprod', + defaultRouteTableId: 'ocid1.routetable..web', + defaultSecurityListId: 'ocid1.securitylist..web', + timeCreated: '2026-06-01T09:00:00Z', + }, + ], + 3: [ + { + id: 'ocid1.vcn.oc1..sgpvcn', + displayName: 'oci-portal-auto-vcn', + lifecycleState: 'AVAILABLE', + compartmentId: 'ocid1.compartment.oc1..root', + cidrBlocks: ['10.0.0.0/16'], + ipv6CidrBlocks: ['2603:c020:400d:aa00::/56'], + dnsLabel: 'ociportal', + defaultRouteTableId: 'ocid1.routetable..sgp', + defaultSecurityListId: 'ocid1.securitylist..sgp', + timeCreated: '2026-06-10T00:00:00Z', + }, + ], + 4: [ + { + id: 'ocid1.vcn.oc1..icnvcn', + displayName: 'seoul-vcn', + lifecycleState: 'AVAILABLE', + compartmentId: 'ocid1.compartment.oc1..root', + cidrBlocks: ['192.168.0.0/16'], + ipv6CidrBlocks: [], + dnsLabel: 'seoul', + defaultRouteTableId: 'ocid1.routetable..icn', + defaultSecurityListId: 'ocid1.securitylist..icn', + timeCreated: '2026-06-28T00:00:00Z', + }, + ], +} + +export const mockSubnets: Subnet[] = [ + { + id: 'ocid1.subnet.oc1..pub', + vcnId: 'ocid1.vcn.oc1..autovcn', + displayName: 'public-subnet', + lifecycleState: 'AVAILABLE', + cidrBlock: '10.0.1.0/24', + ipv6CidrBlock: '2603:c020:8026:9c00::/64', + dnsLabel: 'pub', + prohibitPublicIp: false, + availabilityDomain: '', + routeTableId: 'ocid1.routetable..default', + timeCreated: '2026-05-20T14:31:00Z', + }, + { + id: 'ocid1.subnet.oc1..priv', + vcnId: 'ocid1.vcn.oc1..autovcn', + displayName: 'private-subnet', + lifecycleState: 'AVAILABLE', + cidrBlock: '10.0.2.0/24', + ipv6CidrBlock: '', + dnsLabel: 'priv', + prohibitPublicIp: true, + availabilityDomain: '', + routeTableId: 'ocid1.routetable..default', + timeCreated: '2026-05-21T10:00:00Z', + }, + { + id: 'ocid1.subnet.oc1..webpub', + vcnId: 'ocid1.vcn.oc1..webvcn', + displayName: 'web-public-subnet', + lifecycleState: 'AVAILABLE', + cidrBlock: '10.10.1.0/24', + ipv6CidrBlock: '', + dnsLabel: 'webpub', + prohibitPublicIp: false, + availabilityDomain: '', + routeTableId: 'ocid1.routetable..web', + timeCreated: '2026-06-01T09:05:00Z', + }, +] + +export const mockSecurityLists: SecurityList[] = [ + { + id: 'ocid1.securitylist..default', + vcnId: 'ocid1.vcn.oc1..autovcn', + displayName: 'Default Security List for oci-portal-auto-vcn', + lifecycleState: 'AVAILABLE', + ingressRules: [ + { protocol: 'all', source: '0.0.0.0/0', isStateless: false }, + { protocol: 'all', source: '::/0', isStateless: false }, + ], + egressRules: [ + { protocol: 'all', destination: '0.0.0.0/0', isStateless: false }, + { protocol: 'all', destination: '::/0', isStateless: false }, + ], + timeCreated: '2026-05-20T14:30:30Z', + }, + { + id: 'ocid1.securitylist..strictweb', + vcnId: 'ocid1.vcn.oc1..autovcn', + displayName: 'strict-web', + lifecycleState: 'AVAILABLE', + ingressRules: [ + { + protocol: '6', + source: '0.0.0.0/0', + isStateless: false, + portMin: 22, + portMax: 22, + description: 'ssh', + }, + { + protocol: '6', + source: '0.0.0.0/0', + isStateless: false, + portMin: 80, + portMax: 80, + description: 'http', + }, + { + protocol: '6', + source: '0.0.0.0/0', + isStateless: false, + portMin: 443, + portMax: 443, + description: 'https', + }, + ], + egressRules: [{ protocol: 'all', destination: '0.0.0.0/0', isStateless: false }], + timeCreated: '2026-06-01T09:10:00Z', + }, +] + +export const mockIpv6Report: Ipv6Report = { + steps: [ + { step: 'vcn-ipv6-cidr', status: 'done', detail: '2603:c020:8026:9c00::/56' }, + { step: 'subnet-ipv6-cidr', status: 'done', detail: 'public-subnet 2603:c020:8026:9c00::/64' }, + { step: 'route-ipv6-default', status: 'skipped', detail: '::/0 -> oci-portal-igw 已存在' }, + { step: 'seclist-ipv6-egress', status: 'done', detail: 'egress ::/0 allowed' }, + ], +} + +export const mockBootVolumes: Record = { + 1: [ + { + id: 'ocid1.bootvolume..webfra01', + displayName: 'web-fra-01 (Boot Volume)', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + sizeInGBs: 50, + vpusPerGB: 10, + imageId: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2404', + imageName: 'Canonical-Ubuntu-24.04-aarch64-2026.05.15-0', + attachedInstanceId: 'ocid1.instance.oc1..web-fra-02', + attachedInstanceName: 'web-fra-02', + timeCreated: '2026-05-20T14:32:00Z', + }, + { + id: 'ocid1.bootvolume..webfra02', + displayName: 'web-fra-02 (Boot Volume)', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + sizeInGBs: 50, + vpusPerGB: 10, + imageId: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2404', + imageName: 'Canonical-Ubuntu-24.04-aarch64-2026.05.15-0', + attachedInstanceId: 'ocid1.instance.oc1..web-fra-02', + attachedInstanceName: 'web-fra-02', + timeCreated: '2026-05-22T10:00:00Z', + }, + { + id: 'ocid1.bootvolume..oldweb', + displayName: 'old-web (Boot Volume)', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + sizeInGBs: 47, + vpusPerGB: 10, + imageId: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2204', + imageName: 'Canonical-Ubuntu-22.04-2026.04.02-0', + attachedInstanceId: '', + timeCreated: '2026-04-10T00:00:00Z', + }, + ], + 3: [ + { + id: 'ocid1.bootvolume..dbsgp01', + displayName: 'db-sgp-01 (Boot Volume)', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:AP-SINGAPORE-1-AD-1', + sizeInGBs: 100, + vpusPerGB: 20, + imageId: 'ocid1.image.oc1.ap-singapore-1.ol9', + imageName: 'Oracle-Linux-9.5-2026.06.01-0', + attachedInstanceId: 'ocid1.instance.oc1..db-sgp-01', + timeCreated: '2026-06-10T00:00:00Z', + }, + ], + 4: [ + { + id: 'ocid1.bootvolume..proxyicn', + displayName: 'proxy-icn-01 (Boot Volume)', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:AP-SEOUL-1-AD-1', + sizeInGBs: 50, + vpusPerGB: 10, + imageId: 'ocid1.image.oc1.ap-seoul-1.ubuntu2404', + imageName: 'Canonical-Ubuntu-24.04-Minimal-aarch64-2026.06.18-0', + attachedInstanceId: 'ocid1.instance.oc1..proxy-icn-01', + timeCreated: '2026-06-28T00:00:00Z', + }, + ], +} + +export const mockBvAttachments: BootVolumeAttachment[] = [ + { + id: 'ocid1.bootvolumeattachment..a1', + instanceId: 'ocid1.instance.oc1..web-fra-01', + bootVolumeId: 'ocid1.bootvolume..webfra01', + lifecycleState: 'ATTACHED', + displayName: '', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + }, +] + +export const mockVolAttachments: VolumeAttachment[] = [ + { + id: 'ocid1.volumeattachment..v1', + instanceId: 'ocid1.instance.oc1..web-fra-01', + volumeId: 'ocid1.volume..datavol01', + lifecycleState: 'ATTACHED', + displayName: 'data-vol-01', + device: '/dev/oracleoci/oraclevdb', + isReadOnly: false, + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + }, +] + +/** 按配置 id 组织的 compartment mock(不含租户根) */ +export const mockCompartments: Record = { + 1: [ + { + id: 'ocid1.compartment.oc1..dev', + name: 'dev', + description: '开发环境资源', + parentId: 'ocid1.tenancy.oc1..BCDE主号xxxx', + lifecycleState: 'ACTIVE', + timeCreated: '2026-05-20T00:00:00Z', + }, + { + id: 'ocid1.compartment.oc1..prodnet', + name: 'prod-network', + description: '生产网络资源', + parentId: 'ocid1.tenancy.oc1..BCDE主号xxxx', + lifecycleState: 'ACTIVE', + timeCreated: '2026-06-01T00:00:00Z', + }, + ], +} + +export const mockOverview: Overview = { + tenants: { total: 7, alive: 5, dead: 2, byType: { paid: 1, trial: 2, free: 4 } }, + check: { + hasActiveTask: true, + coveredConfigs: 7, + totalConfigs: 7, + instanceCount: 8, + lastCheckedAt: '2026-07-03T03:00:04+08:00', + }, + cost: { + hasActiveTask: true, + coveredConfigs: 3, + currency: 'USD', + total: 43.6, + days: [ + { day: '2026-06-27', amount: 5.2 }, + { day: '2026-06-28', amount: 5.8 }, + { day: '2026-06-29', amount: 5.4 }, + { day: '2026-06-30', amount: 6.1 }, + { day: '2026-07-01', amount: 8.0 }, + { day: '2026-07-02', amount: 7.7 }, + { day: '2026-07-03', amount: 6.8 }, + ], + }, + tasks: { total: 6, active: 4, healthCheck: 2, cost: 1, snatch: 3 }, +} + +/** OCI 全区域(可订阅列表用,比已订阅的 mockRegionSubs 更全) */ +// 与后端一致:全局按 alias 字典序 +export const mockRegions: RegionInfo[] = [ + { alias: 'Australia East (Sydney)', key: 'SYD', name: 'ap-sydney-1' }, + { alias: 'France Central (Paris)', key: 'CDG', name: 'eu-paris-1' }, + { alias: 'Germany Central (Frankfurt)', key: 'FRA', name: 'eu-frankfurt-1' }, + { alias: 'Japan East (Tokyo)', key: 'NRT', name: 'ap-tokyo-1' }, + { alias: 'Netherlands NW (Amsterdam)', key: 'AMS', name: 'eu-amsterdam-1' }, + { alias: 'Singapore (Singapore)', key: 'SIN', name: 'ap-singapore-1' }, + { alias: 'South Korea Central (Seoul)', key: 'ICN', name: 'ap-seoul-1' }, + { alias: 'UK South (London)', key: 'LHR', name: 'uk-london-1' }, + { alias: 'US East (Ashburn)', key: 'IAD', name: 'us-ashburn-1' }, + { alias: 'US West (Phoenix)', key: 'PHX', name: 'us-phoenix-1' }, + { alias: 'US West (San Jose)', key: 'SJC', name: 'us-sanjose-1' }, +] + +export const mockImages: ImageInfo[] = [ + { + id: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2404arm', + displayName: 'Canonical-Ubuntu-24.04-aarch64-2026.04.30-1', + operatingSystem: 'Canonical Ubuntu', + operatingSystemVersion: '24.04', + sizeInMBs: 47694, + timeCreated: '2026-04-30T00:00:00Z', + }, + { + id: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2404armold', + displayName: 'Canonical-Ubuntu-24.04-aarch64-2026.01.15-0', + operatingSystem: 'Canonical Ubuntu', + operatingSystemVersion: '24.04', + sizeInMBs: 47512, + timeCreated: '2026-01-15T00:00:00Z', + }, + { + id: 'ocid1.image.oc1.eu-frankfurt-1.ubuntu2404x86', + displayName: 'Canonical-Ubuntu-24.04-2026.04.30-1', + operatingSystem: 'Canonical Ubuntu', + operatingSystemVersion: '24.04', + sizeInMBs: 51230, + timeCreated: '2026-04-30T00:00:00Z', + }, + { + id: 'ocid1.image.oc1.eu-frankfurt-1.ol9arm', + displayName: 'Oracle-Linux-9.5-aarch64-2026.05.15-0', + operatingSystem: 'Oracle Linux', + operatingSystemVersion: '9.5', + sizeInMBs: 42100, + timeCreated: '2026-05-15T00:00:00Z', + }, + { + id: 'ocid1.image.oc1.eu-frankfurt-1.debian12', + displayName: 'Debian-12-aarch64-2026.03.20-0', + operatingSystem: 'Debian', + operatingSystemVersion: '12', + sizeInMBs: 38900, + timeCreated: '2026-03-20T00:00:00Z', + }, +] + +export const mockAds = [ + 'RssO:EU-FRANKFURT-1-AD-1', + 'RssO:EU-FRANKFURT-1-AD-2', + 'RssO:EU-FRANKFURT-1-AD-3', +] + +export const mockShapes: ComputeShape[] = [ + { + name: 'VM.Standard.A1.Flex', + billingType: 'LIMITED_FREE', + isFlexible: true, + ocpusMin: 1, + ocpusMax: 80, + memoryMinGBs: 1, + memoryMaxGBs: 512, + processorDescription: '3.0 GHz Ampere® Altra™', + }, + { + name: 'VM.Standard.E2.1.Micro', + billingType: 'ALWAYS_FREE', + isFlexible: false, + ocpus: 1, + memoryInGBs: 1, + processorDescription: '2.0 GHz AMD EPYC™ 7551', + }, + { + name: 'VM.Standard.E4.Flex', + billingType: 'PAID', + isFlexible: true, + ocpusMin: 1, + ocpusMax: 64, + memoryMinGBs: 1, + memoryMaxGBs: 1024, + processorDescription: '2.55 GHz AMD EPYC™ 7J13', + }, + { + name: 'VM.Standard.E5.Flex', + billingType: 'PAID', + isFlexible: true, + ocpusMin: 1, + ocpusMax: 94, + memoryMinGBs: 1, + memoryMaxGBs: 1049, + processorDescription: '2.4 GHz AMD EPYC™ 9J14', + }, + { + name: 'VM.Standard3.Flex', + billingType: 'PAID', + isFlexible: true, + ocpusMin: 1, + ocpusMax: 32, + memoryMinGBs: 1, + memoryMaxGBs: 512, + processorDescription: '2.6 GHz Intel® Xeon® Platinum 8358', + }, + { + name: 'BM.Standard.E4.128', + billingType: 'PAID', + isFlexible: false, + ocpus: 128, + memoryInGBs: 2048, + processorDescription: '2.55 GHz AMD EPYC™ 7J13', + }, +] + +export const mockVolumes: Record = { + 1: [ + { + id: 'ocid1.volume..datavol01', + displayName: 'data-vol-01', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + sizeInGBs: 100, + vpusPerGB: 10, + timeCreated: '2026-05-25T00:00:00Z', + }, + { + id: 'ocid1.volume..datavol02', + displayName: 'data-vol-02', + lifecycleState: 'AVAILABLE', + availabilityDomain: 'RssO:EU-FRANKFURT-1-AD-1', + sizeInGBs: 50, + vpusPerGB: 10, + timeCreated: '2026-06-15T00:00:00Z', + }, + ], +} + +export const mockConsoleConns: ConsoleConnection[] = [ + { + id: 'ocid1.instanceconsoleconnection..c1', + instanceId: 'ocid1.instance.oc1..web-fra-01', + lifecycleState: 'ACTIVE', + connectionString: + 'ssh -o ProxyCommand="ssh -W %h:%p -p 443 ocid1.instanceconsoleconnection..c1@instance-console.eu-frankfurt-1.oci.oraclecloud.com" ocid1.instance.oc1..web-fra-01', + vncConnectionString: + 'ssh -o ProxyCommand="ssh -W %h:%p -p 443 ocid1.instanceconsoleconnection..c1@instance-console.eu-frankfurt-1.oci.oraclecloud.com" -N -L localhost:5900:ocid1.instance.oc1..web-fra-01:5900 ocid1.instance.oc1..web-fra-01', + }, + // DELETED 终态记录:OCI 会保留一段时间,前端应过滤不展示 + { + id: 'ocid1.instanceconsoleconnection..c0', + instanceId: 'ocid1.instance.oc1..web-fra-01', + lifecycleState: 'DELETED', + connectionString: + 'ssh -o ProxyCommand="ssh -W %h:%p -p 443 ocid1.instanceconsoleconnection..c0@instance-console.eu-frankfurt-1.oci.oraclecloud.com" ocid1.instance.oc1..web-fra-01', + vncConnectionString: + 'ssh -o ProxyCommand="ssh -W %h:%p -p 443 ocid1.instanceconsoleconnection..c0@instance-console.eu-frankfurt-1.oci.oraclecloud.com" -N -L localhost:5900:ocid1.instance.oc1..web-fra-01:5900 ocid1.instance.oc1..web-fra-01', + }, +] + +// mock 态可变:保存后回读能看到变化,便于演示设置流程 +export const mockTelegramSetting: TelegramSetting = { + enabled: true, + chatId: '123456789', + tokenSet: true, + tokenTail: 'k9Qw', +} + +// mock 态可变:保存后回读能看到变化;缺省全开与后端语义一致 +export const mockNotifyEvents: NotifyEventsSetting = { + taskFail: true, + taskRecover: true, + snatchSuccess: true, + tenantDead: true, + taskStop: true, + loginLock: true, + logEventInstance: true, + logEventIdentity: true, + logEventPolicy: true, + logEventRegion: true, + logEventLogin: true, +} + +// mock 态可变:保存后回读能看到变化;缺省 3 与后端语义一致 +export const mockTaskSetting: TaskSetting = { snatchAuthFailLimit: 3 } + +export const mockSecuritySetting: SecuritySetting = { + loginFailLimit: 5, + loginLockMinutes: 15, + ipRateRps: 10, + ipRateBurst: 30, + realIpHeader: '', + appUrl: '', +} + +// 系统操作日志:倒序排列;含失败登录(401)与失败写操作,便于验收状态标红 +export const mockSystemLogs: SystemLog[] = [ + { + id: 14, + username: 'admin', + method: 'POST', + path: '/api/v1/oci-configs/:id/instances', + status: 200, + durationMs: 9572, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-06T15:42:11+08:00', + }, + { + id: 13, + username: 'admin', + method: 'PUT', + path: '/api/v1/settings/telegram', + status: 200, + durationMs: 12, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-06T15:38:47+08:00', + }, + { + id: 12, + username: 'root', + method: 'POST', + path: '/api/v1/auth/login', + status: 401, + durationMs: 87, + clientIp: '198.51.100.23', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + errMsg: '用户名、密码或验证码错误', + createdAt: '2026-07-06T14:03:52+08:00', + }, + { + id: 11, + username: 'admin', + method: 'POST', + path: '/api/v1/auth/login', + status: 200, + durationMs: 104, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-06T09:15:20+08:00', + }, + { + id: 10, + username: 'admin', + method: 'DELETE', + path: '/api/v1/oci-configs/:id/instances/:instanceId', + status: 200, + durationMs: 3210, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-05T22:40:05+08:00', + }, + { + id: 9, + username: 'admin', + method: 'POST', + path: '/api/v1/oci-configs/:id/instances/:instanceId/action', + status: 502, + durationMs: 1840, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + errMsg: '请求失败', + createdAt: '2026-07-05T22:31:44+08:00', + }, + { + id: 8, + username: 'admin', + method: 'POST', + path: '/api/v1/tasks', + status: 201, + durationMs: 9, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-05T20:12:30+08:00', + }, + { + id: 7, + username: 'admin', + method: 'PUT', + path: '/api/v1/tasks/:id', + status: 200, + durationMs: 8, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-05T20:10:02+08:00', + }, + { + id: 6, + username: 'admin', + method: 'POST', + path: '/api/v1/oci-configs/:id/verify', + status: 200, + durationMs: 2380, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-04T18:55:19+08:00', + }, + { + id: 5, + username: 'admin', + method: 'PUT', + path: '/api/v1/oci-configs/:id', + status: 200, + durationMs: 15, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-04T18:52:40+08:00', + }, + { + id: 4, + username: 'admin', + method: 'POST', + path: '/api/v1/oci-configs', + status: 400, + durationMs: 6, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + errMsg: '请求失败', + createdAt: '2026-07-04T18:50:08+08:00', + }, + { + id: 3, + username: 'guest', + method: 'POST', + path: '/api/v1/auth/login', + status: 401, + durationMs: 92, + clientIp: '198.51.100.23', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + errMsg: '用户名、密码或验证码错误', + createdAt: '2026-07-04T03:22:37+08:00', + }, + { + id: 2, + username: 'admin', + method: 'POST', + path: '/api/v1/oci-configs/:id/subnets', + status: 200, + durationMs: 1420, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-03T16:08:59+08:00', + }, + { + id: 1, + username: 'admin', + method: 'POST', + path: '/api/v1/oci-configs/:id/vcns', + status: 200, + durationMs: 1980, + clientIp: '203.0.113.7', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', + createdAt: '2026-07-03T16:07:12+08:00', + }, +] + +// 日志回传事件:倒序排列;含一条未解析与一条截断样本,便于验收徽标与占位 +export const mockLogEvents: LogEvent[] = [ + { + id: 3, + ociConfigId: 1, + messageId: 'msg-0003', + eventType: '', + source: '', + sourceIp: '', + eventTime: null, + payload: 'opaque test message published from console', + truncated: false, + processed: false, + receivedAt: '2026-07-07T10:12:40+08:00', + }, + { + id: 2, + ociConfigId: 5, + messageId: 'msg-0002', + eventType: 'com.oraclecloud.ComputeApi.TerminateInstance', + source: 'ComputeApi', + sourceIp: '203.0.113.7', + eventTime: '2026-07-07T09:58:02+08:00', + payload: + '{"eventType":"com.oraclecloud.ComputeApi.TerminateInstance","source":"ComputeApi","eventTime":"2026-07-07T01:58:02Z","data":{"compartmentName":"root","resourceName":"tokyo-a1-1"}}', + truncated: true, + processed: true, + receivedAt: '2026-07-07T09:58:10+08:00', + }, + { + id: 1, + ociConfigId: 1, + messageId: 'msg-0001', + eventType: 'com.oraclecloud.identityControlPlane.UpdateUser', + source: 'IdentityControlPlane', + sourceIp: '198.51.100.23', + eventTime: '2026-07-06T22:14:33+08:00', + payload: + '{"eventType":"com.oraclecloud.identityControlPlane.UpdateUser","source":"IdentityControlPlane","eventTime":"2026-07-06T14:14:33Z","data":{"resourceName":"api.user"}}', + truncated: false, + processed: true, + receivedAt: '2026-07-06T22:14:40+08:00', + }, +] + +// 回传回调地址:mock 态可变,生成 / 撤销后回读可见 +export const mockLogWebhooks = new Map() + +// OCI 侧回传链路:mock 态记录「已一键创建」的租户 +export const mockLogRelays = new Map() diff --git a/src/api/networks.ts b/src/api/networks.ts new file mode 100644 index 0000000..afcf8ad --- /dev/null +++ b/src/api/networks.ts @@ -0,0 +1,66 @@ +import { mockIpv6Report, mockSecurityLists, mockSubnets, mockVcns } from './mock' +import { mockOn, mocked, request } from './request' +import type { CreateVcnRequest, Ipv6Report, SecurityList, Subnet, Vcn } from '@/types/api' + +export function listVcns(cfgId: number, region?: string, compartmentId?: string): Promise { + if (mockOn) return mocked(compartmentId ? [] : (mockVcns[cfgId] ?? [])) + return request(`/oci-configs/${cfgId}/vcns`, { query: { region, compartmentId } }) +} + +export function getVcn(cfgId: number, vcnId: string, region?: string): Promise { + if (mockOn) { + const found = Object.values(mockVcns) + .flat() + .find((v) => v.id === vcnId) + return found ? mocked(found) : Promise.reject(new Error('VCN 不存在')) + } + return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}`, { query: { region } }) +} + +export function createVcn(cfgId: number, body: CreateVcnRequest): Promise { + if (mockOn) return mocked({ ...mockVcns[1][0], id: 'ocid1.vcn..new', displayName: body.displayName }, 900) + return request(`/oci-configs/${cfgId}/vcns`, { method: 'POST', body }) +} + +export function renameVcn( + cfgId: number, + vcnId: string, + displayName: string, + region?: string, +): Promise { + if (mockOn) return mocked({ ...mockVcns[1][0], displayName }) + return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}`, { + method: 'PUT', + body: { region: region ?? '', displayName }, + }) +} + +export function deleteVcn(cfgId: number, vcnId: string, region?: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}`, { + method: 'DELETE', + query: { region }, + }) +} + +export function enableVcnIpv6(cfgId: number, vcnId: string, region?: string): Promise { + if (mockOn) return mocked(mockIpv6Report, 1500) + return request(`/oci-configs/${cfgId}/vcns/${encodeURIComponent(vcnId)}/enable-ipv6`, { + method: 'POST', + body: { region: region ?? '' }, + }) +} + +export function listSubnets(cfgId: number, vcnId: string, region?: string): Promise { + if (mockOn) return mocked(mockSubnets.filter((s) => s.vcnId === vcnId || vcnId === '')) + return request(`/oci-configs/${cfgId}/subnets`, { query: { vcnId, region } }) +} + +export function listSecurityLists( + cfgId: number, + vcnId: string, + region?: string, +): Promise { + if (mockOn) return mocked(mockSecurityLists.filter((s) => s.vcnId === vcnId || vcnId === '')) + return request(`/oci-configs/${cfgId}/security-lists`, { query: { vcnId, region } }) +} diff --git a/src/api/proxies.ts b/src/api/proxies.ts new file mode 100644 index 0000000..29e57c1 --- /dev/null +++ b/src/api/proxies.ts @@ -0,0 +1,90 @@ +import { mockProxies } from './mock' +import { mockOn, mocked, request } from './request' +import type { ProxyImportResult, ProxyInfo, ProxyInput } from '@/types/api' + +export function listProxies(): Promise<{ items: ProxyInfo[] }> { + if (mockOn) return mocked({ items: mockProxies }) + return request('/proxies') +} + +/** mock 端复刻后端自动命名:留空生成 {type}-{host}:{port},冲突加序号 */ +function mockAutoName(body: ProxyInput): string { + const base = `${body.type}-${body.host}:${body.port}` + let name = base + for (let i = 2; mockProxies.some((p) => p.name === name); i++) name = `${base}-${i}` + return name +} + +function mockCreate(body: ProxyInput): ProxyInfo { + const created: ProxyInfo = { + id: Math.max(0, ...mockProxies.map((p) => p.id)) + 1, + name: body.name?.trim() || mockAutoName(body), + type: body.type, + host: body.host, + port: body.port, + username: body.username ?? '', + passwordSet: !!body.password, + country: '', + city: '', + geoAt: '', + usedBy: 0, + createdAt: new Date().toISOString(), + } + mockProxies.push(created) + return created +} + +export function createProxy(body: ProxyInput): Promise { + if (mockOn) return mocked(mockCreate(body)) + return request('/proxies', { method: 'POST', body }) +} + +export function updateProxy(id: number, body: ProxyInput): Promise { + if (mockOn) { + const found = mockProxies.find((p) => p.id === id) + if (!found) return Promise.reject(new Error('代理不存在')) + Object.assign(found, body, { + name: body.name?.trim() || found.name, + passwordSet: body.password ? true : found.passwordSet, + }) + return mocked({ ...found }) + } + return request(`/proxies/${id}`, { method: 'PUT', body }) +} + +export function deleteProxy(id: number): Promise { + if (mockOn) { + const i = mockProxies.findIndex((p) => p.id === id) + if (i >= 0) mockProxies.splice(i, 1) + return mocked(undefined) + } + return request(`/proxies/${id}`, { method: 'DELETE' }) +} + +/** 批量导入:每行 scheme://[user:pass@]host:port 或 host:port[:user:pass] */ +export function importProxies(text: string): Promise { + if (mockOn) { + const created = text + .split('\n') + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith('#')) + .map((l) => { + const m = /^(?:(socks5|http|https):\/\/)?(?:[^@]+@)?([^:@/]+):(\d+)/.exec(l) + return m ? mockCreate({ type: m[1] ?? 'socks5', host: m[2], port: Number(m[3]) }) : null + }) + .filter((p): p is ProxyInfo => !!p) + return mocked({ created, failed: [] }) + } + return request('/proxies/import', { method: 'POST', body: { text } }) +} + +/** 手动重测出口地区,同步返回最新视图 */ +export function probeProxy(id: number): Promise { + if (mockOn) { + const found = mockProxies.find((p) => p.id === id) + if (!found) return Promise.reject(new Error('代理不存在')) + Object.assign(found, { country: '日本', city: '大阪市', geoAt: new Date().toISOString() }) + return mocked({ ...found }) + } + return request(`/proxies/${id}/probe`, { method: 'POST' }) +} diff --git a/src/api/request.ts b/src/api/request.ts new file mode 100644 index 0000000..d4b7706 --- /dev/null +++ b/src/api/request.ts @@ -0,0 +1,66 @@ +import { useAuthStore } from '@/stores/auth' + +const BASE = '/api/v1' + +export class ApiError extends Error { + status: number + + constructor(status: number, message: string) { + super(message) + this.status = status + } +} + +interface RequestOptions { + method?: string + body?: unknown + query?: Record +} + +function buildUrl(path: string, query?: RequestOptions['query']): string { + if (!query) return BASE + path + const qs = new URLSearchParams() + for (const [k, v] of Object.entries(query)) { + if (v !== undefined && v !== '') qs.set(k, String(v)) + } + const s = qs.toString() + return s ? `${BASE}${path}?${s}` : BASE + path +} + +async function parseError(resp: Response): Promise { + let message = `请求失败(${resp.status})` + try { + const body = (await resp.json()) as { error?: string; hint?: string } + if (body.error) message = body.hint ? `${body.hint}|${body.error}` : body.error + } catch { + /* 非 JSON 响应体,保留默认消息 */ + } + if (resp.status === 401) useAuthStore().logout() + // 全局限速(429)整页拦截;登录接口的 429 是账号锁定,留在表单内提示 + if (resp.status === 429 && !resp.url.includes('/auth/login') && location.pathname !== '/blocked') { + location.assign('/blocked') + } + throw new ApiError(resp.status, message) +} + +/** 统一请求封装:注入 JWT、401 登出、错误转 ApiError */ +export async function request(path: string, opts: RequestOptions = {}): Promise { + const auth = useAuthStore() + const headers: Record = { 'Content-Type': 'application/json' } + if (auth.token) headers.Authorization = `Bearer ${auth.token}` + const resp = await fetch(buildUrl(path, opts.query), { + method: opts.method ?? 'GET', + headers, + body: opts.body === undefined ? undefined : JSON.stringify(opts.body), + }) + if (!resp.ok) await parseError(resp) + if (resp.status === 204) return undefined as T + return (await resp.json()) as T +} + +export const mockOn = import.meta.env.VITE_MOCK === '1' + +/** mock 数据延时返回,模拟网络延迟 */ +export function mocked(data: T, ms = 200): Promise { + return new Promise((resolve) => setTimeout(() => resolve(structuredClone(data)), ms)) +} diff --git a/src/api/settings.ts b/src/api/settings.ts new file mode 100644 index 0000000..4ba8f26 --- /dev/null +++ b/src/api/settings.ts @@ -0,0 +1,152 @@ +import { + mockNotifyEvents, + mockSecuritySetting, + mockSystemLogs, + mockTaskSetting, + mockTelegramSetting, +} from './mock' +import { mockOn, mocked, request } from './request' +import type { + AboutInfo, + NotifyEventsSetting, + NotifyTemplateItem, + SecuritySetting, + SystemLogPage, + TaskSetting, + TelegramSetting, +} from '@/types/api' + +export function getTelegramSetting(): Promise { + if (mockOn) return mocked({ ...mockTelegramSetting }) + return request('/settings/telegram') +} + +export interface UpdateTelegramBody { + enabled: boolean + /** 缺省表示沿用已保存的 token,空串表示清除 */ + botToken?: string + chatId: string +} + +export function updateTelegramSetting(body: UpdateTelegramBody): Promise { + if (mockOn) { + mockTelegramSetting.enabled = body.enabled + mockTelegramSetting.chatId = body.chatId + if (body.botToken !== undefined) { + mockTelegramSetting.tokenSet = body.botToken !== '' + mockTelegramSetting.tokenTail = body.botToken.length >= 8 ? body.botToken.slice(-4) : '' + } + return mocked({ ...mockTelegramSetting }, 400) + } + return request('/settings/telegram', { method: 'PUT', body }) +} + +export function testTelegram(): Promise { + if (mockOn) return mocked(undefined, 800) + return request('/settings/telegram/test', { method: 'POST' }) +} + +export function getNotifyEvents(): Promise { + if (mockOn) return mocked({ ...mockNotifyEvents }) + return request('/settings/notify-events') +} + +/** 全量保存五个事件开关,返回最新值 */ +export function updateNotifyEvents(body: NotifyEventsSetting): Promise { + if (mockOn) { + Object.assign(mockNotifyEvents, body) + return mocked({ ...mockNotifyEvents }, 400) + } + return request('/settings/notify-events', { method: 'PUT', body }) +} + +export function listNotifyTemplates(): Promise { + if (mockOn) + return mocked([ + { + kind: 'task_fail', + label: '任务失败', + vars: ['task_name', 'error'], + defaultTemplate: '❌ 任务失败:{{task_name}}\n{{error}}', + template: '', + }, + ]) + return request<{ items: NotifyTemplateItem[] }>('/settings/notify-templates').then((r) => r.items) +} + +/** 保存单个通知模板;空串清除自定义(恢复默认) */ +export function updateNotifyTemplate(kind: string, template: string): Promise { + if (mockOn) return mocked(undefined, 300) + return request(`/settings/notify-templates/${kind}`, { method: 'PUT', body: { template } }) +} + +/** 用示例变量渲染并发送测试消息;template 非空时按编辑中内容渲染(不落库) */ +export function testNotifyTemplate(kind: string, template: string): Promise { + if (mockOn) return mocked(undefined, 600) + return request(`/settings/notify-templates/${kind}/test`, { method: 'POST', body: { template } }) +} + +export function getTaskSetting(): Promise { + if (mockOn) return mocked({ ...mockTaskSetting }) + return request('/settings/task') +} + +/** 保存任务行为设置,返回最新值;阈值越界(1-10 之外)后端返回 400 */ +export function updateTaskSetting(body: TaskSetting): Promise { + if (mockOn) { + Object.assign(mockTaskSetting, body) + return mocked({ ...mockTaskSetting }, 400) + } + return request('/settings/task', { method: 'PUT', body }) +} + +export function getSecuritySetting(): Promise { + if (mockOn) return mocked({ ...mockSecuritySetting }) + return request('/settings/security') +} + +/** 保存安全设置,返回最新值;越界或非法后端返回 400,保存后立即生效 */ +export function updateSecuritySetting(body: SecuritySetting): Promise { + if (mockOn) { + Object.assign(mockSecuritySetting, body) + return mocked({ ...mockSecuritySetting }, 400) + } + return request('/settings/security', { method: 'PUT', body }) +} + +export interface SystemLogParams { + page: number + pageSize: number + /** 关键字,对路径与用户名模糊匹配 */ + keyword?: string +} + +export function listSystemLogs(params: SystemLogParams): Promise { + if (mockOn) { + const kw = (params.keyword ?? '').toLowerCase() + const filtered = kw + ? mockSystemLogs.filter( + (l) => l.path.toLowerCase().includes(kw) || l.username.toLowerCase().includes(kw), + ) + : mockSystemLogs + const start = (params.page - 1) * params.pageSize + return mocked({ + items: filtered.slice(start, start + params.pageSize), + total: filtered.length, + }) + } + return request('/system-logs', { query: { ...params } }) +} + +// ---- 关于 ---- +/** 构建与运行环境信息(设置 · 关于) */ +export function getAbout(): Promise { + if (mockOn) + return mocked({ + version: 'v1.0.0', + buildTime: '2026-07-09T12:00Z', + goVersion: 'go1.26.4', + platform: 'linux/amd64', + }) + return request('/about') +} diff --git a/src/api/tasks.ts b/src/api/tasks.ts new file mode 100644 index 0000000..2a761f8 --- /dev/null +++ b/src/api/tasks.ts @@ -0,0 +1,75 @@ +import { mockTaskLogs, mockTasks } from './mock' +import { mockOn, mocked, request } from './request' +import type { Task, TaskLog, TaskStatus } from '@/types/api' + +export function listTasks(): Promise { + if (mockOn) return mocked(mockTasks) + return request('/tasks') +} + +export function getTask(id: number): Promise { + if (mockOn) { + const found = mockTasks.find((t) => t.id === id) + return found ? mocked(found) : Promise.reject(new Error('任务不存在')) + } + return request(`/tasks/${id}`) +} + +export interface CreateTaskRequest { + name: string + type: string + cronExpr: string + payload: Record +} + +export function createTask(body: CreateTaskRequest): Promise { + if (mockOn) { + const created: Task = { + ...mockTasks[0], + id: Math.max(...mockTasks.map((t) => t.id)) + 1, + name: body.name, + type: body.type as Task['type'], + cronExpr: body.cronExpr, + payload: JSON.stringify(body.payload), + status: 'active', + lastRunAt: null, + runCount: 0, + } + mockTasks.push(created) + return mocked(created, 600) + } + return request('/tasks', { method: 'POST', body }) +} + +export interface UpdateTaskRequest { + name?: string + cronExpr?: string + payload?: Record + status?: TaskStatus +} + +export function updateTask(id: number, body: UpdateTaskRequest): Promise { + if (mockOn) { + const base = mockTasks.find((t) => t.id === id) ?? mockTasks[0] + return mocked({ ...base, status: body.status ?? base.status }) + } + return request(`/tasks/${id}`, { method: 'PUT', body }) +} + +export function deleteTask(id: number): Promise { + if (mockOn) return mocked(undefined) + return request(`/tasks/${id}`, { method: 'DELETE' }) +} + +export function listTaskLogs(id: number, limit = 50): Promise { + if (mockOn) return mocked(mockTaskLogs[id] ?? []) + return request(`/tasks/${id}/logs`, { query: { limit } }) +} + +export function runTask(id: number): Promise { + if (mockOn) { + const logs = mockTaskLogs[id] + return mocked(logs?.[0] ?? { id: 0, taskId: id, success: true, message: 'ok', durationMs: 100, createdAt: '' }, 1000) + } + return request(`/tasks/${id}/run`, { method: 'POST' }) +} diff --git a/src/api/tenant.ts b/src/api/tenant.ts new file mode 100644 index 0000000..923dc70 --- /dev/null +++ b/src/api/tenant.ts @@ -0,0 +1,344 @@ +import { + mockAuditEvents, + mockIdentitySetting, + mockIdps, + mockLimits, + mockPolicies, + mockRecipients, + mockSignOnRules, + mockSubscriptions, + mockUserDetails, + mockUsers, +} from './mock' +import { mockOn, mocked, request } from './request' +import type { + AuditEventsResult, + IamUser, + IamUserDetail, + IdentityProvider, + IdentitySetting, + LimitItem, + LimitService, + NotificationRecipients, + PasswordPolicy, + SignOnRule, + SubscriptionDetail, + SubscriptionSummary, +} from '@/types/api' + +// ---- 配额 ---- +export interface LimitsQuery { + service?: string + region?: string + name?: string + scopeType?: 'GLOBAL' | 'REGION' | 'AD' + availabilityDomain?: string + /** 服务端过滤上限为 0 的配额项,查用量时不占并发名额 */ + nonZero?: boolean + withAvailability?: boolean +} + +export function listLimits(id: number, query: LimitsQuery = {}): Promise { + if (mockOn) { + if (query.service === 'regions') + return mocked([{ name: 'subscribed-region-count', scopeType: 'GLOBAL', value: 3 }]) + const needle = (query.name ?? '').toLowerCase() + return mocked( + mockLimits + .filter((l) => !needle || l.name.toLowerCase().includes(needle)) + .filter((l) => !query.scopeType || l.scopeType === query.scopeType) + .filter((l) => !query.nonZero || l.value > 0), + ) + } + return request(`/oci-configs/${id}/limits`, { query: { ...query } }) +} + +export function listLimitServices(id: number): Promise { + if (mockOn) + return mocked([ + { name: 'compute', description: 'Compute' }, + { name: 'vcn', description: 'Virtual Cloud Network' }, + { name: 'block-storage', description: 'Block Volume' }, + ]) + return request(`/oci-configs/${id}/limits/services`) +} + +// ---- 订阅 ---- +export function listSubscriptions(id: number): Promise { + if (mockOn) + return mocked( + mockSubscriptions.map((d) => ({ + id: d.id, + serviceName: d.serviceName, + timeCreated: d.timeCreated, + timeUpdated: d.timeUpdated, + })), + ) + return request(`/oci-configs/${id}/subscriptions`) +} + +export function getSubscription(id: number, subscriptionId: string): Promise { + if (mockOn) + return mocked(mockSubscriptions.find((d) => d.id === subscriptionId) ?? mockSubscriptions[0]) + return request(`/oci-configs/${id}/subscriptions/${subscriptionId}`) +} + +// ---- 用户 ---- +export function listUsers(id: number): Promise { + if (mockOn) return mocked(mockUsers) + return request(`/oci-configs/${id}/users`) +} + +// 编辑表单预填充用;不在身份域的经典用户返回 inDomain: false +export function getUserDetail(id: number, userId: string): Promise { + if (mockOn) + return mocked( + mockUserDetails[userId] ?? { + inDomain: false, + givenName: '', + familyName: '', + isDomainAdmin: false, + inAdminGroup: false, + }, + ) + return request(`/oci-configs/${id}/users/${userId}`) +} + +export interface CreateUserBody { + name: string + givenName: string + familyName: string + description?: string + email?: string + /** 同时勾选时后端先授身份域管理员,再加入管理员组 */ + grantDomainAdmin?: boolean + addToAdminGroup?: boolean +} + +export function createUser(id: number, body: CreateUserBody): Promise { + if (mockOn) + return mocked({ ...mockUsers[2], id: 'ocid1.user..new', name: body.name, email: body.email ?? '' }) + return request(`/oci-configs/${id}/users`, { method: 'POST', body }) +} + +export interface UpdateUserBody { + description?: string + email?: string + givenName?: string + familyName?: string + /** 三态:true 授予/加入,false 撤销/移出,缺省不动 */ + grantDomainAdmin?: boolean + addToAdminGroup?: boolean +} + +export function updateUser(id: number, userId: string, body: UpdateUserBody): Promise { + if (mockOn) { + const u = mockUsers.find((x) => x.id === userId) ?? mockUsers[2] + if (body.description !== undefined) u.description = body.description + if (body.email !== undefined) u.email = body.email + return mocked({ ...u }) + } + return request(`/oci-configs/${id}/users/${userId}`, { method: 'PUT', body }) +} + +export function deleteUser(id: number, userId: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${id}/users/${userId}`, { method: 'DELETE' }) +} + +export function resetUserPassword(id: number, userId: string): Promise<{ password: string }> { + if (mockOn) return mocked({ password: 'Xy9#kQ2m$Lp8' }) + return request(`/oci-configs/${id}/users/${userId}/reset-password`, { method: 'POST' }) +} + +export function clearUserMfa(id: number, userId: string): Promise<{ deletedTotpDevices: number }> { + if (mockOn) return mocked({ deletedTotpDevices: 1 }) + return request(`/oci-configs/${id}/users/${userId}/mfa-devices`, { method: 'DELETE' }) +} + +export function deleteUserApiKeys( + id: number, + userId: string, + includeCurrent = false, +): Promise<{ deletedApiKeys: number }> { + if (mockOn) return mocked({ deletedApiKeys: 1 }) + return request(`/oci-configs/${id}/users/${userId}/api-keys`, { + method: 'DELETE', + query: { includeCurrent }, + }) +} + +// ---- 审计日志 ---- +/** 实时查询租户 OCI 审计事件:hours ∈ [1,72] 首查; + * 截断续查传上次响应的 start/end 与 nextPage(绝对窗+游标断点续翻) */ +export function listAuditEvents( + id: number, + opts: { hours?: number; region?: string; start?: string; end?: string; page?: string } = {}, +): Promise { + // mock 下选 1 小时窗演示截断提示 + if (mockOn) + return mocked( + { + items: mockAuditEvents, + truncated: opts.hours === 1, + start: '2026-07-06T10:00:00Z', + end: '2026-07-07T10:00:00Z', + }, + 300, + ) + const { hours = 24, region, start, end, page } = opts + const query = start && end ? { region, start, end, page } : { hours, region } + return request(`/oci-configs/${id}/audit-events`, { query }) +} + +/** 取回单条审计事件的原始 JSON;404 表示缓存过期且重查未找回(前端降级展示精简字段) */ +export function getAuditEventDetail( + id: number, + q: { eventId: string; eventTime: string; region?: string }, +): Promise<{ raw: unknown }> { + if (mockOn) return mocked({ raw: { eventId: q.eventId, mock: true, note: '演示原始事件' } }, 200) + return request(`/oci-configs/${id}/audit-events/detail`, { query: q }) +} + +// ---- 域设置 ---- +export function getNotificationRecipients(id: number): Promise { + if (mockOn) return mocked(mockRecipients) + return request(`/oci-configs/${id}/notification-recipients`) +} + +export function updateNotificationRecipients( + id: number, + recipients: string[], +): Promise { + if (mockOn) return mocked({ recipients, testModeEnabled: recipients.length > 0 }) + return request(`/oci-configs/${id}/notification-recipients`, { + method: 'PUT', + body: { recipients }, + }) +} + +export function listPasswordPolicies(id: number): Promise { + if (mockOn) return mocked(mockPolicies) + return request(`/oci-configs/${id}/password-policies`) +} + +export function updatePasswordPolicy( + id: number, + policyId: string, + body: { passwordExpiresAfter?: number; minLength?: number; numPasswordsInHistory?: number }, +): Promise { + if (mockOn) return mocked({ ...mockPolicies[0], ...body }) + return request(`/oci-configs/${id}/password-policies/${policyId}`, { method: 'PUT', body }) +} + +// ---- 身份设置 ---- +export function getIdentitySetting(id: number): Promise { + if (mockOn) return mocked({ ...mockIdentitySetting }) + return request(`/oci-configs/${id}/identity-settings`) +} + +export function updateIdentitySetting( + id: number, + primaryEmailRequired: boolean, +): Promise { + if (mockOn) { + mockIdentitySetting.primaryEmailRequired = primaryEmailRequired + return mocked({ ...mockIdentitySetting }) + } + return request(`/oci-configs/${id}/identity-settings`, { + method: 'PUT', + body: { primaryEmailRequired }, + }) +} + +export async function downloadSamlMetadata(id: number): Promise { + if (mockOn) { + const blob = new Blob([''], { type: 'application/xml' }) + triggerDownload(blob, 'oci-domain-saml-metadata.xml') + return + } + const { useAuthStore } = await import('@/stores/auth') + const auth = useAuthStore() + const resp = await fetch(`/api/v1/oci-configs/${id}/saml-metadata`, { + headers: auth.token ? { Authorization: `Bearer ${auth.token}` } : {}, + }) + if (!resp.ok) throw new Error(`下载失败(${resp.status})`) + triggerDownload(await resp.blob(), 'oci-domain-saml-metadata.xml') +} + +function triggerDownload(blob: Blob, filename: string) { + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = filename + a.click() + URL.revokeObjectURL(url) +} + +// ---- Federation ---- +export interface CreateIdpBody { + name: string + metadata: string + description?: string + /** logo:data URI 或外链 URL */ + iconUrl?: string + nameIdFormat?: string + /** 非空表示按断言属性映射;空则按 SAML NameID 映射 */ + assertionAttribute?: string + userStoreAttribute?: string + jitEnabled?: boolean + jitCreate?: boolean + jitUpdate?: boolean + jitAssignAdminGroup?: boolean + jitMapEmail?: boolean +} + +export function createIdp(id: number, body: CreateIdpBody): Promise { + if (mockOn) { + const created = { + ...mockIdps[1], + id: `idp-${mockIdps.length + 1}`, + name: body.name, + enabled: false, + } + mockIdps.push(created) + return mocked(created, 900) + } + return request(`/oci-configs/${id}/identity-providers`, { method: 'POST', body }) +} + +export function listIdps(id: number): Promise { + if (mockOn) return mocked(mockIdps) + return request(`/oci-configs/${id}/identity-providers`) +} + +export function activateIdp(id: number, idpId: string, enabled: boolean): Promise { + if (mockOn) return mocked({ ...mockIdps[0], enabled }) + return request(`/oci-configs/${id}/identity-providers/${idpId}/activate`, { + method: 'POST', + body: { enabled }, + }) +} + +export function deleteIdp(id: number, idpId: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${id}/identity-providers/${idpId}`, { method: 'DELETE' }) +} + +export function listSignOnRules(id: number): Promise { + if (mockOn) return mocked(mockSignOnRules) + return request(`/oci-configs/${id}/sign-on-rules`) +} + +export function createSignOnExemption(id: number, identityProviderId: string): Promise { + if (mockOn) return mocked(mockSignOnRules[0]) + return request(`/oci-configs/${id}/sign-on-exemptions`, { + method: 'POST', + body: { identityProviderId }, + }) +} + +export function deleteSignOnExemption(id: number, ruleId: string): Promise { + if (mockOn) return mocked(undefined) + return request(`/oci-configs/${id}/sign-on-exemptions/${ruleId}`, { method: 'DELETE' }) +} diff --git a/src/assets/main.css b/src/assets/main.css new file mode 100644 index 0000000..fb90bcc --- /dev/null +++ b/src/assets/main.css @@ -0,0 +1,260 @@ +/* 设计 token 唯一 JS 来源在 src/theme/tokens.ts,两处取值保持同步 */ +@import 'tailwindcss'; + +@theme { + --color-bg: #f5f4ed; + --color-card: #faf9f5; + --color-white: #ffffff; /* 语义:输入 / 嵌入面,暗色下为深灰而非纯白 */ + --color-ink: #141413; + --color-ink-2: #5e5d59; + --color-ink-3: #87867f; + --color-line: #e8e6dc; + --color-line-soft: #f0eee6; + --color-row-hover: #f1efe8; + --color-wash: #efede4; /* 侧栏 / 图标按钮悬浮底 */ + --color-wash-deep: #ece9df; /* 侧栏激活 / 头像底 */ + --color-accent: #c96442; + --color-accent-hover: #ad4f2e; + --color-chart: #c15f3c; + --color-ok: #788c5d; + --color-err: #b53333; + --color-info: #6a9bcc; + --color-warn: #a87514; + + --font-sans: + -apple-system, 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Noto Sans SC', sans-serif; + --font-serif: Georgia, 'Times New Roman', 'Songti SC', serif; + --font-mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; +} + +/* 暗色主题:html.dark 由 stores/app.ts 挂载,色值与 tokens.ts 的 darkTokens 同步 */ +html.dark { + --color-bg: #191817; + --color-card: #201f1e; + --color-white: #262524; + --color-ink: #eceae4; + --color-ink-2: #a8a69e; + --color-ink-3: #807e76; + --color-line: #3a3835; + --color-line-soft: #2c2b29; + --color-row-hover: #292825; + --color-wash: #2b2a27; + --color-wash-deep: #33312d; + --color-accent: #d97757; + --color-accent-hover: #e08b6d; + --color-chart: #d97757; + --color-ok: #8fa574; + --color-err: #d06060; + --color-info: #7fabd6; + --color-warn: #c29135; + color-scheme: dark; +} + +html, +body { + background: var(--color-bg); + color: var(--color-ink); + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* 全局滚动条瘦身:透明轨道 + 细圆角滑块,明暗主题跟随 ink 变量 */ +* { + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--color-ink) 22%, transparent) transparent; +} +::-webkit-scrollbar { + width: 7px; + height: 7px; + background: transparent; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--color-ink) 22%, transparent); + border-radius: 4px; +} +::-webkit-scrollbar-thumb:hover { + background: color-mix(in srgb, var(--color-ink) 38%, transparent); +} + +/* 页面标题统一 serif */ +.page-title { + font-family: var(--font-serif); + font-size: 24px; + font-weight: 600; +} + +/* 卡片:圆角 8、细边框、whisper 阴影,无渐变; + overflow hidden 防止内部表格等方角内容溢出圆角 */ +.panel { + background: var(--color-card); + border: 1px solid var(--color-line); + border-radius: 8px; + box-shadow: 0 1px 2px rgba(20, 20, 19, 0.04); + min-width: 0; + overflow: hidden; +} + +.mono { + font-family: var(--font-mono); + font-size: 12.5px; + letter-spacing: -0.2px; +} + +/* NDataTable 分页器默认贴表格底边,补出与卡片底部的间隔 */ +.n-data-table .n-data-table__pagination { + padding: 4px 18px 14px; +} + +/* 租户下拉双行选项:菜单项上下留白拉开行距,避免相邻两租户挤在一起 */ +.n-base-select-option .cfg-tenancy-sub { + margin-top: 1px; +} +.n-base-select-option__content:has(.cfg-tenancy-sub) { + padding: 6px 0; +} +/* NSelect 选中框(收起态)只显示别名单行,租户名辅行隐藏 */ +.n-base-selection-label .cfg-tenancy-sub { + display: none; +} + +/* AppInputNumber(方案 B 内嵌步进):上下箭头收进右侧独立竖栏—— + 竖栏占满框高、border-left 与框体结构化分隔,上下两格中缝分隔线, + 悬停格底 wash;suffix 内 minus / add 恒为最后两个子元素(add 上 minus 下)。 */ +.app-input-number .n-input .n-input-wrapper { + padding-right: 34px; /* 26px 竖栏 + 文本安全间距 */ +} +.app-input-number .n-input__suffix { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 26px; + margin: 0; + display: grid; + grid-template-rows: 1fr 1fr; + border-left: 1px solid var(--color-line-soft); +} +.app-input-number .n-input__suffix > :last-child { + grid-row: 1; /* add 在上 */ + border-bottom: 1px solid var(--color-line-soft); + border-top-right-radius: 5px; +} +.app-input-number .n-input__suffix > :nth-last-child(2) { + grid-row: 2; /* minus 在下 */ + border-bottom-right-radius: 5px; +} +.app-input-number .n-input__suffix .n-button { + /* 箭头颜色跟随文本弱色而非按钮主题色:NButton 自带 --n-text-color + 内联声明会盖过外层 color,必须直接覆盖变量 */ + --n-icon-size: 9px; + --n-text-color: var(--color-ink-3); + --n-text-color-hover: var(--color-ink); + --n-text-color-pressed: var(--color-ink); + --n-text-color-focus: var(--color-ink); + width: 100%; + height: 100%; + border-radius: inherit; + color: var(--color-ink-3); +} +.app-input-number .n-input__suffix .n-button:not(.n-button--disabled):hover { + background: var(--color-wash); + color: var(--color-ink); +} + +/* useToast 可展开详情:toast 内嵌代码块(标签 + 复制 + 内滚 pre)。 + message 经 teleport 渲染于 body,组件 scoped 样式不可达,故置于全局。 */ +.toast-body { + max-width: 420px; +} +.toast-line { + display: flex; + align-items: baseline; + gap: 10px; +} +.toast-toggle { + flex: none; + cursor: pointer; + font-size: 12px; + color: var(--color-accent); + user-select: none; +} +.toast-toggle:hover { + text-decoration: underline; +} +.toast-code { + margin-top: 8px; + overflow: hidden; + border: 1px solid var(--color-line); + border-radius: 8px; + background: var(--color-wash); +} +.toast-code-head { + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--color-line-soft); + padding: 4px 8px 4px 10px; +} +.toast-tag { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; +} +.toast-tag-error { + color: var(--color-err); +} +.toast-tag-warning { + color: var(--color-warn); +} +.toast-copy { + cursor: pointer; + border: none; + background: none; + padding: 2px 4px; + font-size: 11px; + color: var(--color-ink-3); +} +.toast-copy:hover { + color: var(--color-ink); +} +.toast-code-pre { + margin: 0; + max-height: 180px; + overflow: auto; + padding: 8px 10px; + font-family: var(--font-mono); + font-size: 11.5px; + line-height: 1.55; + white-space: pre-wrap; + word-break: break-all; + color: var(--color-ink-2); +} + +/* 全屏页轨道环(FullPageBackdrop 与登录页品牌区共用): + spin 动画占用 transform,环定位只能用 left/top(calc 同心),不能用 translate。 */ +.fpb-ring { + position: absolute; + border-radius: 50%; + border: 1.5px solid var(--color-line); + pointer-events: none; +} +.fpb-ring-dash { + border-style: dashed; + border-color: color-mix(in srgb, var(--color-line) 70%, var(--color-accent) 30%); +} +@media (prefers-reduced-motion: no-preference) { + .fpb-spin { + animation: fpb-spin 52s linear infinite; + } + @keyframes fpb-spin { + to { + transform: rotate(360deg); + } + } +} diff --git a/src/components/AccountTypeBadge.vue b/src/components/AccountTypeBadge.vue new file mode 100644 index 0000000..dd62db9 --- /dev/null +++ b/src/components/AccountTypeBadge.vue @@ -0,0 +1,26 @@ + + + diff --git a/src/components/AppInputNumber.vue b/src/components/AppInputNumber.vue new file mode 100644 index 0000000..e0a41d8 --- /dev/null +++ b/src/components/AppInputNumber.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/AppLogo.vue b/src/components/AppLogo.vue new file mode 100644 index 0000000..87c56b9 --- /dev/null +++ b/src/components/AppLogo.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/CostChart.vue b/src/components/CostChart.vue new file mode 100644 index 0000000..0553ae2 --- /dev/null +++ b/src/components/CostChart.vue @@ -0,0 +1,60 @@ + + + diff --git a/src/components/DeadPlaceholder.vue b/src/components/DeadPlaceholder.vue new file mode 100644 index 0000000..a7d6ebc --- /dev/null +++ b/src/components/DeadPlaceholder.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/DetailFieldList.vue b/src/components/DetailFieldList.vue new file mode 100644 index 0000000..f20fad4 --- /dev/null +++ b/src/components/DetailFieldList.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/components/DetailHero.vue b/src/components/DetailHero.vue new file mode 100644 index 0000000..9ec38b9 --- /dev/null +++ b/src/components/DetailHero.vue @@ -0,0 +1,34 @@ + + + diff --git a/src/components/EmptyCard.vue b/src/components/EmptyCard.vue new file mode 100644 index 0000000..ab02d92 --- /dev/null +++ b/src/components/EmptyCard.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/FilePicker.vue b/src/components/FilePicker.vue new file mode 100644 index 0000000..1ce94f9 --- /dev/null +++ b/src/components/FilePicker.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/components/FootNote.vue b/src/components/FootNote.vue new file mode 100644 index 0000000..eb505f8 --- /dev/null +++ b/src/components/FootNote.vue @@ -0,0 +1,5 @@ + diff --git a/src/components/FormField.vue b/src/components/FormField.vue new file mode 100644 index 0000000..aa55214 --- /dev/null +++ b/src/components/FormField.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/components/FormModal.vue b/src/components/FormModal.vue new file mode 100644 index 0000000..d7172d7 --- /dev/null +++ b/src/components/FormModal.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/FormSection.vue b/src/components/FormSection.vue new file mode 100644 index 0000000..274d28d --- /dev/null +++ b/src/components/FormSection.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/FullPageBackdrop.vue b/src/components/FullPageBackdrop.vue new file mode 100644 index 0000000..114e9ef --- /dev/null +++ b/src/components/FullPageBackdrop.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/src/components/JsonBlock.vue b/src/components/JsonBlock.vue new file mode 100644 index 0000000..dcca6fc --- /dev/null +++ b/src/components/JsonBlock.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/components/LifecycleBadge.vue b/src/components/LifecycleBadge.vue new file mode 100644 index 0000000..67ee033 --- /dev/null +++ b/src/components/LifecycleBadge.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/components/OauthProviderIcon.vue b/src/components/OauthProviderIcon.vue new file mode 100644 index 0000000..2525464 --- /dev/null +++ b/src/components/OauthProviderIcon.vue @@ -0,0 +1,25 @@ + + + diff --git a/src/components/OcidText.vue b/src/components/OcidText.vue new file mode 100644 index 0000000..f63cc8d --- /dev/null +++ b/src/components/OcidText.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/components/PageHeader.vue b/src/components/PageHeader.vue new file mode 100644 index 0000000..0e8293e --- /dev/null +++ b/src/components/PageHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/src/components/RenameModal.vue b/src/components/RenameModal.vue new file mode 100644 index 0000000..67ca963 --- /dev/null +++ b/src/components/RenameModal.vue @@ -0,0 +1,45 @@ + + + diff --git a/src/components/StatusBadge.vue b/src/components/StatusBadge.vue new file mode 100644 index 0000000..b3e5949 --- /dev/null +++ b/src/components/StatusBadge.vue @@ -0,0 +1,48 @@ + + + diff --git a/src/components/TenantPicker.vue b/src/components/TenantPicker.vue new file mode 100644 index 0000000..d5f88de --- /dev/null +++ b/src/components/TenantPicker.vue @@ -0,0 +1,198 @@ + + + diff --git a/src/components/ToggleCard.vue b/src/components/ToggleCard.vue new file mode 100644 index 0000000..aa5eb6d --- /dev/null +++ b/src/components/ToggleCard.vue @@ -0,0 +1,17 @@ + + + diff --git a/src/components/ai/AiChannelPanel.vue b/src/components/ai/AiChannelPanel.vue new file mode 100644 index 0000000..ca13d85 --- /dev/null +++ b/src/components/ai/AiChannelPanel.vue @@ -0,0 +1,250 @@ + + + diff --git a/src/components/ai/AiKeyPanel.vue b/src/components/ai/AiKeyPanel.vue new file mode 100644 index 0000000..0d1af54 --- /dev/null +++ b/src/components/ai/AiKeyPanel.vue @@ -0,0 +1,226 @@ + + + diff --git a/src/components/config/EditConfigModal.vue b/src/components/config/EditConfigModal.vue new file mode 100644 index 0000000..5bef6f9 --- /dev/null +++ b/src/components/config/EditConfigModal.vue @@ -0,0 +1,194 @@ + + + diff --git a/src/components/config/ImportConfigModal.vue b/src/components/config/ImportConfigModal.vue new file mode 100644 index 0000000..c03936e --- /dev/null +++ b/src/components/config/ImportConfigModal.vue @@ -0,0 +1,213 @@ + + + diff --git a/src/components/instance/AttachVnicModal.vue b/src/components/instance/AttachVnicModal.vue new file mode 100644 index 0000000..ae8bd28 --- /dev/null +++ b/src/components/instance/AttachVnicModal.vue @@ -0,0 +1,146 @@ + + + diff --git a/src/components/instance/AttachVolumeModal.vue b/src/components/instance/AttachVolumeModal.vue new file mode 100644 index 0000000..ff8a27e --- /dev/null +++ b/src/components/instance/AttachVolumeModal.vue @@ -0,0 +1,114 @@ + + + diff --git a/src/components/instance/ConsoleConnModal.vue b/src/components/instance/ConsoleConnModal.vue new file mode 100644 index 0000000..60cde77 --- /dev/null +++ b/src/components/instance/ConsoleConnModal.vue @@ -0,0 +1,74 @@ + + + diff --git a/src/components/instance/CreateInstanceModal.vue b/src/components/instance/CreateInstanceModal.vue new file mode 100644 index 0000000..44750c5 --- /dev/null +++ b/src/components/instance/CreateInstanceModal.vue @@ -0,0 +1,105 @@ + + + diff --git a/src/components/instance/EditBootVolumeModal.vue b/src/components/instance/EditBootVolumeModal.vue new file mode 100644 index 0000000..b025b80 --- /dev/null +++ b/src/components/instance/EditBootVolumeModal.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/components/instance/InstanceSpecForm.vue b/src/components/instance/InstanceSpecForm.vue new file mode 100644 index 0000000..bb4bc2b --- /dev/null +++ b/src/components/instance/InstanceSpecForm.vue @@ -0,0 +1,535 @@ + + + diff --git a/src/components/instance/InstanceTrafficPanel.vue b/src/components/instance/InstanceTrafficPanel.vue new file mode 100644 index 0000000..a266210 --- /dev/null +++ b/src/components/instance/InstanceTrafficPanel.vue @@ -0,0 +1,140 @@ + + + diff --git a/src/components/instance/ReplaceBootVolumeModal.vue b/src/components/instance/ReplaceBootVolumeModal.vue new file mode 100644 index 0000000..80b3b25 --- /dev/null +++ b/src/components/instance/ReplaceBootVolumeModal.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/components/instance/ResizeModal.vue b/src/components/instance/ResizeModal.vue new file mode 100644 index 0000000..92ffc47 --- /dev/null +++ b/src/components/instance/ResizeModal.vue @@ -0,0 +1,146 @@ + + + diff --git a/src/components/instance/SerialConsolePanel.vue b/src/components/instance/SerialConsolePanel.vue new file mode 100644 index 0000000..43550cb --- /dev/null +++ b/src/components/instance/SerialConsolePanel.vue @@ -0,0 +1,332 @@ + + + + + diff --git a/src/components/instance/VnicPanel.vue b/src/components/instance/VnicPanel.vue new file mode 100644 index 0000000..3b695a0 --- /dev/null +++ b/src/components/instance/VnicPanel.vue @@ -0,0 +1,235 @@ + + + diff --git a/src/components/logs/AiCallLogPanel.vue b/src/components/logs/AiCallLogPanel.vue new file mode 100644 index 0000000..1084f5e --- /dev/null +++ b/src/components/logs/AiCallLogPanel.vue @@ -0,0 +1,293 @@ + + + diff --git a/src/components/logs/LogEventPanel.vue b/src/components/logs/LogEventPanel.vue new file mode 100644 index 0000000..d58876c --- /dev/null +++ b/src/components/logs/LogEventPanel.vue @@ -0,0 +1,280 @@ + + + diff --git a/src/components/logs/SystemLogPanel.vue b/src/components/logs/SystemLogPanel.vue new file mode 100644 index 0000000..9efc353 --- /dev/null +++ b/src/components/logs/SystemLogPanel.vue @@ -0,0 +1,219 @@ + + + diff --git a/src/components/network/CreateVcnModal.vue b/src/components/network/CreateVcnModal.vue new file mode 100644 index 0000000..8527058 --- /dev/null +++ b/src/components/network/CreateVcnModal.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/components/proxy/ProxyPanel.vue b/src/components/proxy/ProxyPanel.vue new file mode 100644 index 0000000..a47c6f0 --- /dev/null +++ b/src/components/proxy/ProxyPanel.vue @@ -0,0 +1,386 @@ + + + diff --git a/src/components/settings/AboutTab.vue b/src/components/settings/AboutTab.vue new file mode 100644 index 0000000..a6f6108 --- /dev/null +++ b/src/components/settings/AboutTab.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/src/components/settings/AccountSecurityCard.vue b/src/components/settings/AccountSecurityCard.vue new file mode 100644 index 0000000..6b09bd6 --- /dev/null +++ b/src/components/settings/AccountSecurityCard.vue @@ -0,0 +1,739 @@ + + + + + diff --git a/src/components/settings/NotifyTemplateModal.vue b/src/components/settings/NotifyTemplateModal.vue new file mode 100644 index 0000000..e6c7ffb --- /dev/null +++ b/src/components/settings/NotifyTemplateModal.vue @@ -0,0 +1,162 @@ + + + diff --git a/src/components/storage/AttachBootVolumeModal.vue b/src/components/storage/AttachBootVolumeModal.vue new file mode 100644 index 0000000..8100d2a --- /dev/null +++ b/src/components/storage/AttachBootVolumeModal.vue @@ -0,0 +1,97 @@ + + + diff --git a/src/components/storage/EditBootVolumeModal.vue b/src/components/storage/EditBootVolumeModal.vue new file mode 100644 index 0000000..02f39ad --- /dev/null +++ b/src/components/storage/EditBootVolumeModal.vue @@ -0,0 +1,110 @@ + + + diff --git a/src/components/task/CronEditor.vue b/src/components/task/CronEditor.vue new file mode 100644 index 0000000..991400b --- /dev/null +++ b/src/components/task/CronEditor.vue @@ -0,0 +1,178 @@ + + + diff --git a/src/components/task/TaskCard.vue b/src/components/task/TaskCard.vue new file mode 100644 index 0000000..b4722fb --- /dev/null +++ b/src/components/task/TaskCard.vue @@ -0,0 +1,191 @@ + + + diff --git a/src/components/task/TaskFormModal.vue b/src/components/task/TaskFormModal.vue new file mode 100644 index 0000000..e2b3360 --- /dev/null +++ b/src/components/task/TaskFormModal.vue @@ -0,0 +1,345 @@ + + + diff --git a/src/components/task/TaskTypeIcon.vue b/src/components/task/TaskTypeIcon.vue new file mode 100644 index 0000000..49a8f03 --- /dev/null +++ b/src/components/task/TaskTypeIcon.vue @@ -0,0 +1,50 @@ + + + diff --git a/src/components/task/taskDisplay.ts b/src/components/task/taskDisplay.ts new file mode 100644 index 0000000..c85bf30 --- /dev/null +++ b/src/components/task/taskDisplay.ts @@ -0,0 +1,91 @@ +import type { Task, TaskStatus, TaskType } from '@/types/api' + +/** 抢机任务 payload 的展示视图(count 为剩余台数,totalCount 为目标台数) */ +export interface SnatchInfo { + cfgId: number + total: number + remaining: number + done: number + shape: string + ocpus?: number + memoryInGBs?: number + bootGb?: number + region?: string + displayName?: string +} + +export function parseSnatch(task: Task): SnatchInfo | null { + if (task.type !== 'snatch') return null + try { + const p = JSON.parse(task.payload || '{}') as Record + const inst = (p.instance ?? {}) as Record + const remaining = Math.max(0, Number(p.count ?? 0)) + // 旧任务无 totalCount:已完成的按剩余 0 处理,进行中的退化为 remaining + const total = Number(p.totalCount ?? 0) || (task.status === 'succeeded' ? remaining || 1 : remaining || 1) + const done = task.status === 'succeeded' ? total : Math.max(0, total - remaining) + return { + cfgId: Number(p.ociConfigId ?? 0), + total, + remaining, + done, + shape: String(inst.shape ?? '—'), + ocpus: inst.ocpus as number | undefined, + memoryInGBs: inst.memoryInGBs as number | undefined, + bootGb: inst.bootVolumeSizeGBs as number | undefined, + region: inst.region as string | undefined, + displayName: inst.displayName as string | undefined, + } + } catch { + return null + } +} + +/** 测活 / 成本任务的租户范围(空 = 全部租户) */ +export function scopeCfgIds(task: Task): number[] { + try { + const p = JSON.parse(task.payload || '{}') as Record + return Array.isArray(p.ociConfigIds) ? (p.ociConfigIds as number[]) : [] + } catch { + return [] + } +} + +/** 常见 cron 形态的中文描述;识别不了返回空串(界面回退只显示表达式) */ +export function cronText(expr: string): string { + const p = expr.trim().split(/\s+/) + if (p.length !== 5) return '' + const [min, hour, dom, mon, dow] = p + if (dom !== '*' || mon !== '*') return '' + const at = (h: string, m: string) => `${h.padStart(2, '0')}:${m.padStart(2, '0')}` + const weekdays = ['日', '一', '二', '三', '四', '五', '六'] + if (dow !== '*') { + const d = Number(dow) + if (!/^\d$/.test(dow) || d > 6 || !/^\d+$/.test(hour) || !/^\d+$/.test(min)) return '' + return `每周${weekdays[d]} ${at(hour, min)}` + } + const minStep = /^\*\/(\d+)$/.exec(min) + if (minStep && hour === '*') return Number(minStep[1]) === 1 ? '每分钟' : `每 ${minStep[1]} 分钟` + if (min === '*' && hour === '*') return '每分钟' + const hourStep = /^\*\/(\d+)$/.exec(hour) + if (hourStep && /^\d+$/.test(min)) return `每 ${hourStep[1]} 小时` + if (/^\d+$/.test(min) && hour === '*') return `每小时第 ${Number(min)} 分` + if (/^\d+$/.test(min) && /^\d+$/.test(hour)) return `每天 ${at(hour, min)}` + return '' +} + +export const TYPE_LABEL: Record = { + snatch: '抢机', + health_check: '测活', + cost: '成本', + ai_probe: 'AI探测', +} + +export const STATUS_META: Record< + TaskStatus, + { label: string; kind: 'run' | 'warn' | 'prov' | 'term' } +> = { + active: { label: '运行中', kind: 'run' }, + paused: { label: '已暂停', kind: 'warn' }, + succeeded: { label: '已完成', kind: 'prov' }, + failed: { label: '已熔断', kind: 'term' }, +} diff --git a/src/components/tenant/AuditLogTab.vue b/src/components/tenant/AuditLogTab.vue new file mode 100644 index 0000000..6ccc749 --- /dev/null +++ b/src/components/tenant/AuditLogTab.vue @@ -0,0 +1,277 @@ + + + diff --git a/src/components/tenant/CostTab.vue b/src/components/tenant/CostTab.vue new file mode 100644 index 0000000..fe1be0e --- /dev/null +++ b/src/components/tenant/CostTab.vue @@ -0,0 +1,106 @@ + + + diff --git a/src/components/tenant/IdpFormModal.vue b/src/components/tenant/IdpFormModal.vue new file mode 100644 index 0000000..092d951 --- /dev/null +++ b/src/components/tenant/IdpFormModal.vue @@ -0,0 +1,218 @@ + + + diff --git a/src/components/tenant/IdpTab.vue b/src/components/tenant/IdpTab.vue new file mode 100644 index 0000000..cc1e53d --- /dev/null +++ b/src/components/tenant/IdpTab.vue @@ -0,0 +1,216 @@ + + + diff --git a/src/components/tenant/MiscTab.vue b/src/components/tenant/MiscTab.vue new file mode 100644 index 0000000..8d423b5 --- /dev/null +++ b/src/components/tenant/MiscTab.vue @@ -0,0 +1,409 @@ + + + diff --git a/src/components/tenant/PolicyEditModal.vue b/src/components/tenant/PolicyEditModal.vue new file mode 100644 index 0000000..ccd2998 --- /dev/null +++ b/src/components/tenant/PolicyEditModal.vue @@ -0,0 +1,94 @@ + + + diff --git a/src/components/tenant/QuotaTab.vue b/src/components/tenant/QuotaTab.vue new file mode 100644 index 0000000..b86b0dc --- /dev/null +++ b/src/components/tenant/QuotaTab.vue @@ -0,0 +1,233 @@ + + + diff --git a/src/components/tenant/SubsTab.vue b/src/components/tenant/SubsTab.vue new file mode 100644 index 0000000..9a9664f --- /dev/null +++ b/src/components/tenant/SubsTab.vue @@ -0,0 +1,297 @@ + + + diff --git a/src/components/tenant/SubscribeRegionModal.vue b/src/components/tenant/SubscribeRegionModal.vue new file mode 100644 index 0000000..0e041ac --- /dev/null +++ b/src/components/tenant/SubscribeRegionModal.vue @@ -0,0 +1,88 @@ + + + diff --git a/src/components/tenant/UserFormModal.vue b/src/components/tenant/UserFormModal.vue new file mode 100644 index 0000000..af278d6 --- /dev/null +++ b/src/components/tenant/UserFormModal.vue @@ -0,0 +1,233 @@ + + + diff --git a/src/components/tenant/UsersTab.vue b/src/components/tenant/UsersTab.vue new file mode 100644 index 0000000..6e18c31 --- /dev/null +++ b/src/components/tenant/UsersTab.vue @@ -0,0 +1,176 @@ + + + diff --git a/src/composables/cron.ts b/src/composables/cron.ts new file mode 100644 index 0000000..1a5e1c0 --- /dev/null +++ b/src/composables/cron.ts @@ -0,0 +1,41 @@ +/** 5 段 cron 单字段匹配:支持 *、n、*\/k、a-b、逗号组合;超出子集返回 false */ +function fieldMatch(field: string, v: number): boolean { + return field.split(',').some((seg) => { + const step = /^\*\/(\d+)$/.exec(seg) + if (step) return Number(step[1]) > 0 && v % Number(step[1]) === 0 + const range = /^(\d+)-(\d+)$/.exec(seg) + if (range) return v >= Number(range[1]) && v <= Number(range[2]) + return seg === '*' || seg === String(v) + }) +} + +/** 计算 5 段 cron 的下次执行时间(本地时区): + * 从下一分钟起逐分钟扫描 8 天,表达式非法或窗口内无匹配返回 null。 + * 日与周同时受限时按 AND 近似(面板生成的表达式不会同时限制)。 */ +export function nextCronRun(expr: string): Date | null { + const p = expr.trim().split(/\s+/) + if (p.length !== 5) return null + const t = new Date() + t.setSeconds(0, 0) + for (let i = 0; i < 8 * 24 * 60; i++) { + t.setMinutes(t.getMinutes() + 1) + if ( + fieldMatch(p[0], t.getMinutes()) && + fieldMatch(p[1], t.getHours()) && + fieldMatch(p[2], t.getDate()) && + fieldMatch(p[3], t.getMonth() + 1) && + fieldMatch(p[4], t.getDay()) + ) + return new Date(t) + } + return null +} + +const pad2 = (n: number) => String(n).padStart(2, '0') + +/** 下次执行的短文案(MM-DD HH:mm);无法计算返回空串 */ +export function nextCronRunLabel(expr: string): string { + const d = nextCronRun(expr) + if (!d) return '' + return `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}` +} diff --git a/src/composables/useActionLabel.ts b/src/composables/useActionLabel.ts new file mode 100644 index 0000000..7ae95b3 --- /dev/null +++ b/src/composables/useActionLabel.ts @@ -0,0 +1,83 @@ +/** + * 系统日志「动作」中文化:路由模板 + 方法 → 人话描述。 + * 精确匹配优先,未命中回退资源段兜底;新增路由不强求登记,兜底可读。 + */ +const exact: Record = { + 'POST /api/v1/auth/login': '登录面板', + 'POST /api/v1/auth/totp/setup': '发起两步验证绑定', + 'POST /api/v1/auth/totp/activate': '启用两步验证', + 'POST /api/v1/auth/totp/disable': '停用两步验证', + 'DELETE /api/v1/auth/identities/:id': '解绑外部身份', + 'PUT /api/v1/settings/telegram': '保存 Telegram 通知配置', + 'POST /api/v1/settings/telegram/test': '发送通知测试消息', + 'PUT /api/v1/settings/notify-events': '保存通知管理开关', + 'PUT /api/v1/settings/task': '保存任务设置', + 'PUT /api/v1/settings/security': '保存安全设置', + 'PUT /api/v1/settings/oauth': '保存登录方式配置', + 'POST /api/v1/oci-configs': '导入租户 API Key', + 'PUT /api/v1/oci-configs/:id': '修改租户配置', + 'DELETE /api/v1/oci-configs/:id': '删除租户配置', + 'POST /api/v1/oci-configs/:id/verify': '租户测活', + 'POST /api/v1/oci-configs/:id/region-subscriptions': '订阅区域', + 'POST /api/v1/oci-configs/:id/instances': '创建实例', + 'PUT /api/v1/oci-configs/:id/instances/:instanceId': '修改实例配置', + 'DELETE /api/v1/oci-configs/:id/instances/:instanceId': '终止实例', + 'POST /api/v1/oci-configs/:id/instances/:instanceId/action': '实例电源操作', + 'POST /api/v1/oci-configs/:id/instances/:instanceId/change-public-ip': '更换公网 IP', + 'POST /api/v1/oci-configs/:id/instances/:instanceId/ipv6-addresses': '添加 IPv6 地址', + 'DELETE /api/v1/oci-configs/:id/instances/:instanceId/ipv6-addresses': '删除 IPv6 地址', + 'POST /api/v1/oci-configs/:id/instances/:instanceId/replace-boot-volume': '更换引导卷', + 'POST /api/v1/oci-configs/:id/users': '创建租户用户', + 'PUT /api/v1/oci-configs/:id/users/:userId': '修改租户用户', + 'DELETE /api/v1/oci-configs/:id/users/:userId': '删除租户用户', + 'POST /api/v1/oci-configs/:id/users/:userId/reset-password': '重置用户密码', + 'DELETE /api/v1/oci-configs/:id/users/:userId/mfa-devices': '移除用户 MFA', + 'DELETE /api/v1/oci-configs/:id/users/:userId/api-keys': '清除用户 API Key', + 'POST /api/v1/oci-configs/:id/log-webhook': '生成回传回调地址', + 'DELETE /api/v1/oci-configs/:id/log-webhook': '撤销回传回调地址', + 'POST /api/v1/oci-configs/:id/log-relay': '一键创建回传链路', + 'DELETE /api/v1/oci-configs/:id/log-relay': '销毁回传链路', + 'POST /api/v1/tasks': '创建后台任务', + 'PUT /api/v1/tasks/:id': '修改后台任务', + 'DELETE /api/v1/tasks/:id': '删除后台任务', + 'POST /api/v1/tasks/:id/run': '手动执行任务', + 'POST /api/v1/webhooks/oci-logs/:secret': '回传消息投递', +} + +/** 资源段兜底词典:精确表未命中时以「动词 + 资源」组合 */ +const nouns: Record = { + vcns: 'VCN', + subnets: '子网', + 'security-lists': '安全列表', + 'boot-volumes': '引导卷', + 'boot-volume-attachments': '引导卷挂载', + 'volume-attachments': '块卷挂载', + 'console-sessions': '控制台会话', + 'console-connections': '控制台连接', + 'notification-recipients': '通知收件人', + 'password-policies': '密码策略', + 'identity-settings': '身份设置', + 'identity-providers': '身份提供方', + 'sign-on-exemptions': 'MFA 豁免', + proxies: '代理', +} + +const verbs: Record = { POST: '创建', PUT: '修改', DELETE: '删除' } + +/** 取路径中最后一个可翻译的资源段(跳过 :param 与动作尾段) */ +function lastNoun(path: string): string { + const segs = path.split('/').filter((s) => s && !s.startsWith(':')) + for (let i = segs.length - 1; i >= 0; i--) { + const zh = nouns[segs[i]] + if (zh) return zh + } + return '' +} + +export function actionLabel(method: string, path: string): string { + const hit = exact[`${method} ${path}`] + if (hit) return hit + const noun = lastNoun(path) + if (noun) return `${verbs[method] ?? method}${noun}` + return '' +} diff --git a/src/composables/useAsync.ts b/src/composables/useAsync.ts new file mode 100644 index 0000000..21a85cd --- /dev/null +++ b/src/composables/useAsync.ts @@ -0,0 +1,36 @@ +import { ref, type Ref } from 'vue' + +interface AsyncState { + data: Ref + loading: Ref + error: Ref + /** silent 静默刷新(不改 loading),用于过渡态轮询等后台刷新,避免闪加载态 */ + run: (opts?: { silent?: boolean }) => Promise +} + +/** 加载一份异步数据:统一 loading / error / 重试语义;并发 run 时只保留最后一次的结果 */ +export function useAsync(fetcher: () => Promise, immediate = true): AsyncState { + const data = ref(null) as Ref + const loading = ref(false) + const error = ref('') + let seq = 0 + + async function run(opts?: { silent?: boolean }) { + const cur = ++seq + if (!opts?.silent) loading.value = true + error.value = '' + try { + const result = await fetcher() + if (cur !== seq) return + data.value = result + } catch (e) { + if (cur !== seq) return + error.value = e instanceof Error ? e.message : String(e) + } finally { + if (cur === seq) loading.value = false + } + } + + if (immediate) void run() + return { data, loading, error, run } +} diff --git a/src/composables/useAutoRefresh.ts b/src/composables/useAutoRefresh.ts new file mode 100644 index 0000000..0636a06 --- /dev/null +++ b/src/composables/useAutoRefresh.ts @@ -0,0 +1,20 @@ +import { onMounted, onUnmounted } from 'vue' + +/** + * 周期静默刷新:每 ms 毫秒调用一次 fn;页面隐藏(标签页切走)或上一轮 + * 尚未完成时跳过本轮,组件卸载自动清理。fn 自行保证静默(不置 loading、不弹错)。 + */ +export function useAutoRefresh(fn: () => Promise, ms = 5000) { + let timer: ReturnType | undefined + let busy = false + onMounted(() => { + timer = setInterval(() => { + if (busy || document.hidden) return + busy = true + void fn().finally(() => { + busy = false + }) + }, ms) + }) + onUnmounted(() => clearInterval(timer)) +} diff --git a/src/composables/useEventLabel.ts b/src/composables/useEventLabel.ts new file mode 100644 index 0000000..60c900e --- /dev/null +++ b/src/composables/useEventLabel.ts @@ -0,0 +1,37 @@ +/** OCI 审计 / 回传事件名的中文标签。 + * 审计事件 eventName 为尾名(LaunchInstance);回传 eventType 为全限定名 + * (com.oraclecloud.ComputeApi.TerminateInstance),先取尾段再匹配,大小写不敏感。 */ +const EVENT_LABELS: Record = { + launchinstance: '创建实例', + terminateinstance: '终止实例', + instanceaction: '实例电源操作', + updateinstance: '修改实例', + changeinstancecompartment: '移动实例', + createuser: '创建用户', + deleteuser: '删除用户', + updateuser: '修改用户', + createapikey: '创建 API 密钥', + deleteapikey: '删除 API 密钥', + uploadapikey: '上传 API 密钥', + updateusercapabilities: '修改用户能力', + createregionsubscription: '订阅区域', + createpolicy: '创建策略', + updatepolicy: '修改策略', + deletepolicy: '删除策略', + interactivelogin: '控制台登录', + createvcn: '创建 VCN', + deletevcn: '删除 VCN', + createsubnet: '创建子网', + deletesubnet: '删除子网', +} + +/** 全限定事件类型的尾段;已是尾名时原样返回 */ +export function eventTail(nameOrType: string): string { + const i = nameOrType.lastIndexOf('.') + return i >= 0 ? nameOrType.slice(i + 1) : nameOrType +} + +/** 事件中文名;未收录返回空串,调用方回退原名 */ +export function eventLabel(nameOrType: string): string { + return EVENT_LABELS[eventTail(nameOrType).toLowerCase()] ?? '' +} diff --git a/src/composables/useFormat.ts b/src/composables/useFormat.ts new file mode 100644 index 0000000..6fd6f59 --- /dev/null +++ b/src/composables/useFormat.ts @@ -0,0 +1,62 @@ +/** 展示格式化工具:时间、字节、OCID 截断 */ + +export function fmtTime(iso: string | null | undefined): string { + if (!iso) return '—' + const d = new Date(iso) + if (Number.isNaN(d.getTime())) return '—' + const pad = (n: number) => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` +} + +export function fmtRelative(iso: string | null | undefined): string { + if (!iso) return '—' + const diff = Date.now() - new Date(iso).getTime() + if (Number.isNaN(diff)) return '—' + const min = Math.floor(diff / 60000) + if (min < 1) return '刚刚' + if (min < 60) return `${min} 分钟前` + const hr = Math.floor(min / 60) + if (hr < 24) return `${hr} 小时前` + return `${Math.floor(hr / 24)} 天前` +} + +export function fmtBytes(n: number): string { + if (n <= 0) return '0 B' + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.min(Math.floor(Math.log2(n) / 10), units.length - 1) + return `${(n / 2 ** (10 * i)).toFixed(1)} ${units[i]}` +} + +/** OCID 中段省略:ocid1.instance.oc1..aaaa…mc25a */ +export function truncOcid(ocid: string, head = 24, tail = 6): string { + if (!ocid || ocid.length <= head + tail + 1) return ocid + return `${ocid.slice(0, head)}…${ocid.slice(-tail)}` +} + +/** 可用域展示:RssO:EU-FRANKFURT-1-AD-1 → AD-1 */ +export function shortAd(ad: string): string { + const m = ad.match(/AD-\d+$/) + return m ? m[0] : ad +} + +/** 区域友好名中括号内的城市短名:Germany Central (Frankfurt) → Frankfurt */ +export function regionCity(label: string): string { + const m = /\(([^)]+)\)/.exec(label) + return m ? m[1] : label +} + +/** 块存储 VPU/GB 档位:0 低成本 / 10 均衡 / 20 高性能 / 30-120 超高性能 */ +export function vpusLabel(vpus: number): string { + if (vpus >= 30) return `${vpus} · 超高性能` + if (vpus >= 20) return `${vpus} · 高性能` + if (vpus >= 10) return `${vpus} · 均衡` + return `${vpus} · 低成本` +} + +/** Oracle 官方风格默认资源名:instance-20260703-2136 / vcn-20260509-0252 */ +export function defaultResourceName(prefix: string): string { + const now = new Date() + const pad = (n: number) => String(n).padStart(2, '0') + const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + return `${prefix}-${date}-${pad(now.getHours())}${pad(now.getMinutes())}` +} diff --git a/src/composables/useParticles.ts b/src/composables/useParticles.ts new file mode 100644 index 0000000..78df927 --- /dev/null +++ b/src/composables/useParticles.ts @@ -0,0 +1,51 @@ +import { onMounted, onUnmounted, type Ref } from 'vue' + +interface ParticleOptions { + /** 粒子数;缺省按画布面积自适应(约每 8000px² 一粒) */ + count?: number +} + +const PALETTE = ['rgba(201,100,66,', 'rgba(135,134,127,', 'rgba(106,155,204,'] + +/** 背景飘浮粒子:缓慢上浮 + 透明度呼吸;遵循 prefers-reduced-motion,卸载即停 */ +export function useParticles(canvasRef: Ref, opts: ParticleOptions = {}) { + let raf = 0 + onMounted(() => { + const cv = canvasRef.value + if (!cv || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return + const ctx = cv.getContext('2d') + if (!ctx) return + cv.width = cv.offsetWidth + cv.height = cv.offsetHeight + const W = cv.width + const H = cv.height + const count = opts.count ?? Math.round((W * H) / 8000) + const ps = Array.from({ length: count }, (_, i) => ({ + x: Math.random() * W, + y: Math.random() * H, + r: 0.7 + Math.random() * 1.9, + v: 0.08 + Math.random() * 0.34, + a: 0.12 + Math.random() * 0.38, + c: PALETTE[i % PALETTE.length], + p: Math.random() * 6.28, + })) + const tick = (t: number) => { + ctx.clearRect(0, 0, W, H) + for (const p of ps) { + p.y -= p.v + if (p.y < -4) { + p.y = H + 4 + p.x = Math.random() * W + } + const alpha = p.a * (0.6 + 0.4 * Math.sin(t / 1300 + p.p)) + ctx.beginPath() + ctx.arc(p.x, p.y, p.r, 0, 6.28318) + ctx.fillStyle = `${p.c}${alpha})` + ctx.fill() + } + raf = requestAnimationFrame(tick) + } + raf = requestAnimationFrame(tick) + }) + onUnmounted(() => cancelAnimationFrame(raf)) +} diff --git a/src/composables/useProxyOptions.ts b/src/composables/useProxyOptions.ts new file mode 100644 index 0000000..2b2e08a --- /dev/null +++ b/src/composables/useProxyOptions.ts @@ -0,0 +1,26 @@ +import { computed } from 'vue' + +import { listProxies } from '@/api/proxies' +import { useAsync } from '@/composables/useAsync' +import type { ProxyInfo } from '@/types/api' + +/** 代理出口地区短文案:未探测=检测中、失败=未知 */ +export function proxyGeoLabel(p: ProxyInfo): string { + if (!p.geoAt) return '检测中' + if (!p.country) return '未知' + return p.city ? `${p.country}·${p.city}` : p.country +} + +/** 出站代理下拉数据源(租户导入 / 修改表单共用): + * 选项 label 为「名称 · 出口地区」,首项为直连哨兵 null。 */ +export function useProxyOptions() { + const proxies = useAsync(listProxies) + const options = computed(() => [ + { label: '直连(不使用代理)', value: null as number | null }, + ...(proxies.data.value?.items ?? []).map((p) => ({ + label: `${p.name} · ${proxyGeoLabel(p)}`, + value: p.id as number | null, + })), + ]) + return { proxies, options } +} diff --git a/src/composables/useRegionAlias.ts b/src/composables/useRegionAlias.ts new file mode 100644 index 0000000..3e88987 --- /dev/null +++ b/src/composables/useRegionAlias.ts @@ -0,0 +1,33 @@ +import { computed, ref, type Ref } from 'vue' + +import { listRegions } from '@/api/configs' +import type { RegionInfo } from '@/types/api' + +/** 模块级缓存:本地静态区域表全站只拉一次 */ +const regions: Ref = ref(null) +let pending: Promise | null = null + +function ensureLoaded() { + if (regions.value || pending) return + pending = listRegions() + .then((list) => { + regions.value = list + }) + .catch(() => { + pending = null + }) +} + +/** 区域友好名:eu-frankfurt-1 / iad(短名 key)→ 友好名,未收录时回退原名 */ +export function useRegionAlias() { + ensureLoaded() + const byName = computed(() => new Map((regions.value ?? []).map((r) => [r.name, r.alias]))) + const byKey = computed( + () => new Map((regions.value ?? []).map((r) => [r.key.toLowerCase(), r.alias])), + ) + function alias(name: string | null | undefined): string { + if (!name) return '—' + return byName.value.get(name) ?? byKey.value.get(name.toLowerCase()) ?? name + } + return { alias, regions } +} diff --git a/src/composables/useToast.ts b/src/composables/useToast.ts new file mode 100644 index 0000000..3b09b2a --- /dev/null +++ b/src/composables/useToast.ts @@ -0,0 +1,82 @@ +import { useMessage } from 'naive-ui' +import { h, ref, type Ref, type VNodeChild } from 'vue' + +type ToastKind = 'error' | 'warning' + +const AUTO_CLOSE_MS = 8000 + +function renderDetail(kind: ToastKind, detail: string, copied: Ref): VNodeChild { + return h('div', { class: 'toast-code' }, [ + h('div', { class: 'toast-code-head' }, [ + h('span', { class: `toast-tag toast-tag-${kind}` }, kind === 'error' ? 'ERROR' : 'WARN'), + h( + 'button', + { + class: 'toast-copy', + onClick: () => { + void navigator.clipboard.writeText(detail) + copied.value = true + }, + }, + copied.value ? '已复制' : '⎘ 复制', + ), + ]), + h('pre', { class: 'toast-code-pre' }, detail), + ]) +} + +function renderToast( + kind: ToastKind, + text: string, + detail: string, + expanded: Ref, + copied: Ref, + toggle: () => void, +): VNodeChild { + const rows: VNodeChild[] = [ + h('div', { class: 'toast-line' }, [ + h('span', text), + h('a', { class: 'toast-toggle', onClick: toggle }, expanded.value ? '收起 ▴' : '展开详情 ▾'), + ]), + ] + if (expanded.value) rows.push(renderDetail(kind, detail, copied)) + return h('div', { class: 'toast-body' }, rows) +} + +/** + * 带可展开详情的消息封装:error/warning 传 detail 时,toast 内嵌 + * 「展开详情」代码块(带复制);展开期间暂停 8s 自动关闭,收起后重新计时。 + */ +export function useToast() { + const message = useMessage() + + function show(kind: ToastKind, text: string, detail?: string) { + if (!detail) { + message[kind](text) + return + } + const expanded = ref(false) + const copied = ref(false) + let timer = 0 + const inst = message[kind]( + () => renderToast(kind, text, detail, expanded, copied, toggle), + { duration: 0, closable: true }, + ) + const arm = () => { + timer = window.setTimeout(() => inst.destroy(), AUTO_CLOSE_MS) + } + function toggle() { + expanded.value = !expanded.value + window.clearTimeout(timer) + if (!expanded.value) arm() + } + arm() + } + + return { + success: (text: string) => message.success(text), + info: (text: string) => message.info(text), + error: (text: string, detail?: string) => show('error', text, detail), + warning: (text: string, detail?: string) => show('warning', text, detail), + } +} diff --git a/src/composables/useTransientPoll.ts b/src/composables/useTransientPoll.ts new file mode 100644 index 0000000..fff5482 --- /dev/null +++ b/src/composables/useTransientPoll.ts @@ -0,0 +1,33 @@ +import { onUnmounted, watch } from 'vue' + +/** OCI 实例的过渡状态:处于这些状态时数据会在短时间内继续变化 */ +export const INSTANCE_TRANSIENT_STATES = [ + 'PROVISIONING', + 'STARTING', + 'STOPPING', + 'TERMINATING', + 'CREATING_IMAGE', + 'MOVING', +] + +/** 挂载关系的过渡状态 */ +export const ATTACHMENT_TRANSIENT_STATES = ['ATTACHING', 'DETACHING'] + +/** + * 过渡态自动轮询:每次数据源更新后,若仍处于过渡态则延时触发一次 refresh, + * 进入稳态自动停止;组件卸载时清理定时器。 + * 依赖 useAsync 每次 run 成功都会产生新的数据引用来驱动 watch。 + */ +export function useTransientPoll( + source: () => T, + isTransient: (v: T) => boolean, + refresh: () => void, + intervalMs = 5000, +) { + let timer: ReturnType | undefined + watch(source, (v) => { + clearTimeout(timer) + if (isTransient(v)) timer = setTimeout(refresh, intervalMs) + }) + onUnmounted(() => clearTimeout(timer)) +} diff --git a/src/layouts/AppLayout.vue b/src/layouts/AppLayout.vue new file mode 100644 index 0000000..a1c33be --- /dev/null +++ b/src/layouts/AppLayout.vue @@ -0,0 +1,231 @@ + + + diff --git a/src/layouts/SidebarNav.vue b/src/layouts/SidebarNav.vue new file mode 100644 index 0000000..cf0d511 --- /dev/null +++ b/src/layouts/SidebarNav.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..c684631 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,12 @@ +import { createPinia } from 'pinia' +import { createApp } from 'vue' + +import App from './App.vue' +import router from './router' + +import './assets/main.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..dcf42d4 --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,103 @@ +import { createRouter, createWebHistory } from 'vue-router' + +import { useAuthStore } from '@/stores/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'login', + component: () => import('@/views/LoginView.vue'), + meta: { public: true }, + }, + { + // 网页 VNC 独立页:从实例详情新标签页打开,无侧栏布局 + path: '/console/vnc/:cfgId/:instanceId', + name: 'vnc-console', + component: () => import('@/views/VncConsoleView.vue'), + }, + { + path: '/', + component: () => import('@/layouts/AppLayout.vue'), + children: [ + { path: '', name: 'overview', component: () => import('@/views/OverviewView.vue') }, + { path: 'tenants', name: 'tenants', component: () => import('@/views/TenantListView.vue') }, + { + path: 'tenants/:id', + name: 'tenant-detail', + component: () => import('@/views/TenantDetailView.vue'), + }, + { + path: 'instances', + name: 'instances', + component: () => import('@/views/InstanceListView.vue'), + }, + { + path: 'instances/:cfgId/:instanceId', + name: 'instance-detail', + component: () => import('@/views/InstanceDetailView.vue'), + }, + { path: 'network', name: 'network', component: () => import('@/views/NetworkListView.vue') }, + { + path: 'network/:cfgId/vcns/:vcnId', + name: 'vcn-detail', + component: () => import('@/views/VcnDetailView.vue'), + }, + { + path: 'storage/boot-volumes', + name: 'boot-volumes', + component: () => import('@/views/BootVolumeListView.vue'), + }, + { path: 'tasks', name: 'tasks', component: () => import('@/views/TaskListView.vue') }, + { + path: 'ai-gateway', + name: 'ai-gateway', + component: () => import('@/views/AiGatewayView.vue'), + }, + { + path: 'tasks/:taskId', + name: 'task-detail', + component: () => import('@/views/TaskDetailView.vue'), + }, + { + path: 'proxies', + name: 'proxies', + component: () => import('@/views/ProxyListView.vue'), + }, + { + path: 'logs', + name: 'logs', + component: () => import('@/views/LogsView.vue'), + }, + { + path: 'settings', + name: 'settings', + component: () => import('@/views/SettingsView.vue'), + }, + ], + }, + { + // 全局限速拦截页:request 层收到非登录接口的 429 时跳转 + path: '/blocked', + name: 'blocked', + component: () => import('@/views/errors/BlockedView.vue'), + meta: { public: true }, + }, + { + path: '/:pathMatch(.*)*', + name: 'not-found', + component: () => import('@/views/errors/NotFoundView.vue'), + meta: { public: true }, + }, + ], +}) + +router.beforeEach((to) => { + const auth = useAuthStore() + if (!to.meta.public && !auth.isAuthed) return { name: 'login', query: { redirect: to.fullPath } } + if (to.name === 'login' && auth.isAuthed) return { name: 'overview' } + return true +}) + +export default router diff --git a/src/stores/app.ts b/src/stores/app.ts new file mode 100644 index 0000000..2e1c7f9 --- /dev/null +++ b/src/stores/app.ts @@ -0,0 +1,37 @@ +import { defineStore } from 'pinia' +import { ref, watch } from 'vue' + +const DARK_KEY = 'oci-portal:dark' + +function initialDark(): boolean { + const saved = localStorage.getItem(DARK_KEY) + if (saved !== null) return saved === '1' + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +/** 跨页面共享的界面状态:侧栏收缩、明暗主题 */ +export const useAppStore = defineStore('app', () => { + const sidebarCollapsed = ref(false) + /** 暗色主题:默认跟随系统,手动切换后按选择记忆 */ + const dark = ref(initialDark()) + + // html.dark 驱动 main.css 的变量覆盖,Naive 主题在 App.vue 跟随 + watch( + dark, + (v) => { + document.documentElement.classList.toggle('dark', v) + localStorage.setItem(DARK_KEY, v ? '1' : '0') + }, + { immediate: true }, + ) + + function toggleSidebar() { + sidebarCollapsed.value = !sidebarCollapsed.value + } + + function toggleDark() { + dark.value = !dark.value + } + + return { sidebarCollapsed, toggleSidebar, dark, toggleDark } +}) diff --git a/src/stores/auth.ts b/src/stores/auth.ts new file mode 100644 index 0000000..f13319a --- /dev/null +++ b/src/stores/auth.ts @@ -0,0 +1,32 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +const TOKEN_KEY = 'oci-portal.token' +const EXPIRES_KEY = 'oci-portal.expiresAt' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem(TOKEN_KEY) ?? '') + const expiresAt = ref(localStorage.getItem(EXPIRES_KEY) ?? '') + + const isAuthed = computed(() => { + if (!token.value) return false + if (!expiresAt.value) return true + return new Date(expiresAt.value).getTime() > Date.now() + }) + + function setSession(newToken: string, newExpiresAt: string) { + token.value = newToken + expiresAt.value = newExpiresAt + localStorage.setItem(TOKEN_KEY, newToken) + localStorage.setItem(EXPIRES_KEY, newExpiresAt) + } + + function logout() { + token.value = '' + expiresAt.value = '' + localStorage.removeItem(TOKEN_KEY) + localStorage.removeItem(EXPIRES_KEY) + } + + return { token, expiresAt, isAuthed, setSession, logout } +}) diff --git a/src/stores/console.ts b/src/stores/console.ts new file mode 100644 index 0000000..0f3c094 --- /dev/null +++ b/src/stores/console.ts @@ -0,0 +1,41 @@ +import { defineStore } from 'pinia' +import { nextTick, ref } from 'vue' + +interface SerialTarget { + cfgId: number + instanceId: string + region?: string + instanceName?: string + rootPassword?: string +} + +/** 全局串行控制台面板状态:面板挂在 AppLayout,切换路由不销毁会话 */ +export const useConsoleStore = defineStore('console', () => { + const show = ref(false) + const cfgId = ref(0) + const instanceId = ref('') + const region = ref(undefined) + const instanceName = ref('') + const rootPassword = ref('') + + /** 打开串行终端;已开着且换了实例时先关旧会话再开新 */ + async function openSerial(target: SerialTarget) { + if (show.value) { + if (instanceId.value === target.instanceId && cfgId.value === target.cfgId) return + show.value = false + await nextTick() + } + cfgId.value = target.cfgId + instanceId.value = target.instanceId + region.value = target.region + instanceName.value = target.instanceName ?? '' + rootPassword.value = target.rootPassword ?? '' + show.value = true + } + + function close() { + show.value = false + } + + return { show, cfgId, instanceId, region, instanceName, rootPassword, openSerial, close } +}) diff --git a/src/stores/scope.ts b/src/stores/scope.ts new file mode 100644 index 0000000..6abf337 --- /dev/null +++ b/src/stores/scope.ts @@ -0,0 +1,186 @@ +import { defineStore } from 'pinia' +import { computed, ref, watch, type ComputedRef, type Ref } from 'vue' + +import { listCachedCompartments, listCachedRegions, listConfigs } from '@/api/configs' +import { useAsync } from '@/composables/useAsync' +import { useRegionAlias } from '@/composables/useRegionAlias' +import type { OciConfigSummary } from '@/types/api' + +const CFG_KEY = 'oci-portal:scope:cfg' +const regionKey = (id: number) => `oci-portal:scope:region:${id}` +const compKey = (id: number) => `oci-portal:scope:comp:${id}` + +interface ScopeState { + cfgId: Ref + region: Ref + compartmentId: Ref + currentConfig: ComputedRef +} + +/** 切换租户:持久化选择并恢复该租户记忆的区域 / 区间(未开启对应开关时锁定默认值) */ +function restoreScope(state: ScopeState) { + const cfg = state.currentConfig.value + if (!cfg) return + localStorage.setItem(CFG_KEY, String(cfg.id)) + state.region.value = (cfg.multiRegion && localStorage.getItem(regionKey(cfg.id))) || cfg.region + state.compartmentId.value = + (cfg.multiCompartment && localStorage.getItem(compKey(cfg.id))) || '' +} + +/** 区域 / 区间变化时按租户记忆到 localStorage */ +function bindPersistence(state: ScopeState) { + watch(state.region, (v) => { + const cfg = state.currentConfig.value + if (cfg && v) localStorage.setItem(regionKey(cfg.id), v) + }) + watch(state.compartmentId, (v) => { + const cfg = state.currentConfig.value + if (!cfg) return + if (v) localStorage.setItem(compKey(cfg.id), v) + else localStorage.removeItem(compKey(cfg.id)) + }) +} + +/** + * 全局资源作用域:租户 / 区域在侧栏选择,区间在资源页选择, + * 实例、网络、存储等资源页共享同一份状态,切页无需重选。 + */ +export const useScopeStore = defineStore('scope', () => { + const cfgId = ref(Number(localStorage.getItem(CFG_KEY) ?? 0)) + const region = ref('') + const compartmentId = ref('') + // 全局分组段已由租户选择器面板内的分组导航取代,清理旧持久化键 + localStorage.removeItem('oci-portal:scope:group') + + const configs = useAsync(listConfigs) + const currentConfig = computed( + () => configs.data.value?.find((c) => c.id === cfgId.value) ?? null, + ) + const state: ScopeState = { cfgId, region, compartmentId, currentConfig } + + /** 区域 / 区间选项来自服务端缓存(未开启对应开关的租户返回锁定值) */ + const regionSubs = useAsync( + () => + currentConfig.value + ? listCachedRegions(currentConfig.value.id).catch(() => []) + : Promise.resolve([]), + false, + ) + const compartments = useAsync( + () => + currentConfig.value + ? listCachedCompartments(currentConfig.value.id).catch(() => []) + : Promise.resolve([]), + false, + ) + + // 配置加载后校正失效租户(首次进入或租户已删除)为第一个 + watch( + () => configs.data.value, + (list) => { + if (!list?.length) return + if (!list.some((c) => c.id === cfgId.value)) cfgId.value = list[0].id + }, + ) + + watch( + () => currentConfig.value?.id, + () => { + if (!currentConfig.value) return + restoreScope(state) + void regionSubs.run() + void compartments.run() + }, + { immediate: true }, + ) + + bindPersistence(state) + + // 订阅列表加载后,记住的区域已退订时回退主区域 + watch( + () => regionSubs.data.value, + (subs) => { + if (!subs?.length) return + if (!subs.some((s) => s.name === region.value)) + region.value = currentConfig.value?.region ?? '' + }, + ) + + // 区间列表加载后,记住的区间已删除 / 关闭多区间时回退根 + watch( + () => compartments.data.value, + (list) => { + if (compartmentId.value && list && !list.some((c) => c.id === compartmentId.value)) + compartmentId.value = '' + }, + ) + + const { alias } = useRegionAlias() + + const cfgOptions = computed( + () => + configs.data.value?.map((c) => ({ + label: c.alias, + value: c.id, + tenancyName: c.tenancyName, + })) ?? [], + ) + + const regionOptions = computed(() => { + const subs = regionSubs.data.value ?? [] + if (!subs.length && currentConfig.value) + return [{ label: alias(currentConfig.value.region), value: currentConfig.value.region }] + return subs.map((s) => ({ + label: s.isHomeRegion ? `${alias(s.name)} · 主` : alias(s.name), + value: s.name, + })) + }) + + const rootLabel = computed(() => + currentConfig.value?.tenancyName + ? `${currentConfig.value.tenancyName}(根)` + : '根 compartment', + ) + const compOptions = computed(() => [ + { label: rootLabel.value, value: '' }, + ...(compartments.data.value ?? []).map((c) => ({ label: c.name, value: c.id })), + ]) + + /** 未开启多区域 / 多区间支持:对应选择器禁用,锁定默认值 */ + const regionDisabled = computed(() => !currentConfig.value?.multiRegion) + const compDisabled = computed(() => !currentConfig.value?.multiCompartment) + + /** 租户增删改 / 测活成功后由管理页调用:重载配置并刷新当前租户的 + * 区域 / 区间选项;失效值由上方各校正 watch 自动回退。 */ + async function refreshConfigs() { + await configs.run() + if (!currentConfig.value) return + void regionSubs.run() + void compartments.run() + } + + /** 把指定租户与其主区域设为全局作用域:先把区域记忆写成主区域, + * 再切租户,restoreScope 读到的记忆即主区域(覆盖此前的区域选择)。 */ + function setGlobalScope(id: number, homeRegion: string) { + localStorage.setItem(regionKey(id), homeRegion) + if (cfgId.value === id) region.value = homeRegion + else cfgId.value = id + } + + return { + refreshConfigs, + setGlobalScope, + cfgId, + region, + compartmentId, + configs, + currentConfig, + regionSubs, + compartments, + cfgOptions, + regionOptions, + compOptions, + regionDisabled, + compDisabled, + } +}) diff --git a/src/theme/naive.ts b/src/theme/naive.ts new file mode 100644 index 0000000..a172a6c --- /dev/null +++ b/src/theme/naive.ts @@ -0,0 +1,97 @@ +import type { GlobalThemeOverrides } from 'naive-ui' + +import { darkTokens, tokens, type ThemeTokens } from './tokens' + +/** Naive UI 主题定制:所有取值来自 tokens.ts,禁止散落硬编码; + * 亮暗两套经同一工厂生成,保证结构一致。 */ +function makeOverrides(t: ThemeTokens): GlobalThemeOverrides { + return { + common: { + primaryColor: t.accent, + primaryColorHover: t.accentHover, + primaryColorPressed: t.accentHover, + primaryColorSuppl: t.accent, + errorColor: t.err, + successColor: t.ok, + infoColor: t.info, + warningColor: t.warn, + textColorBase: t.ink, + textColor1: t.ink, + textColor2: t.ink2, + textColor3: t.ink3, + bodyColor: t.bg, + cardColor: t.card, + modalColor: t.card, + popoverColor: t.white, + inputColor: t.white, + borderColor: t.line, + dividerColor: t.lineSoft, + borderRadius: '6px', + borderRadiusSmall: '4px', + fontFamily: t.fontSans, + fontFamilyMono: t.fontMono, + fontSize: '14px', + hoverColor: t.rowHover, + }, + Button: { + borderRadiusMedium: '6px', + fontWeight: '500', + }, + // 开关表达「启用状态」而非品牌主色:取状态绿,与 StatusBadge 的 + // 运行/已启用一致,也避免与相邻红色危险按钮混淆 + Switch: { + railColorActive: t.ok, + loadingColor: t.ok, + }, + Card: { + borderRadius: '8px', + colorEmbedded: t.card, + }, + DataTable: { + thColor: t.card, + thTextColor: t.ink3, + thFontWeight: '500', + // 表头不做 hover 变色(含排序列) + thColorHover: t.card, + thColorHoverSorting: t.card, + tdColor: t.card, + tdColorHover: t.rowHover, + // 排序激活列不再整列变色,与普通列保持一致 + tdColorSorting: t.card, + thColorSorting: t.card, + tdColorHoverSorting: t.rowHover, + borderColor: t.lineSoft, + thPaddingMedium: '9px 12px', + tdPaddingMedium: '11px 12px', + }, + Input: { + color: t.white, + borderRadius: '6px', + }, + Select: { + peers: { + InternalSelection: { color: t.white, borderRadius: '6px' }, + }, + }, + Tabs: { + tabTextColorLine: t.ink2, + tabTextColorActiveLine: t.ink, + barColor: t.accent, + }, + // Tooltip 统一为白底卡片风,与 NPopover 一致(默认黑底与整体风格冲突); + // NDataTable ellipsis 提示同走此主题 + Tooltip: { + color: t.white, + textColor: t.ink, + borderRadius: '6px', + boxShadow: `0 3px 14px -2px rgba(0, 0, 0, 0.14), 0 0 0 1px ${t.lineSoft}`, + padding: '8px 12px', + }, + Dialog: { borderRadius: '8px' }, + Tag: { borderRadius: '4px' }, + Message: { borderRadius: '6px' }, + } +} + +export const themeOverrides = makeOverrides(tokens) +export const darkThemeOverrides = makeOverrides(darkTokens) diff --git a/src/theme/tokens.ts b/src/theme/tokens.ts new file mode 100644 index 0000000..2cc2a0e --- /dev/null +++ b/src/theme/tokens.ts @@ -0,0 +1,53 @@ +/** + * 设计 token 的 JS 来源,与 src/assets/main.css 的 @theme / html.dark 保持同步。 + * 色板与排版规范见 .trellis/spec/oci-portal-dash/frontend/styling-design-tokens.md(Anthropic 风格)。 + */ +export const tokens = { + bg: '#F5F4ED', + card: '#FAF9F5', + white: '#FFFFFF', + ink: '#141413', + ink2: '#5E5D59', + ink3: '#87867F', + line: '#E8E6DC', + lineSoft: '#F0EEE6', + rowHover: '#F1EFE8', + wash: '#EFEDE4', + washDeep: '#ECE9DF', + accent: '#C96442', + accentHover: '#AD4F2E', + chart: '#C15F3C', + ok: '#788C5D', + err: '#B53333', + info: '#6A9BCC', + warn: '#A87514', + fontSans: + "-apple-system, 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Noto Sans SC', sans-serif", + fontSerif: "Georgia, 'Times New Roman', 'Songti SC', serif", + fontMono: "ui-monospace, 'SF Mono', Menlo, Consolas, monospace", +} + +export type ThemeTokens = typeof tokens + +/** 暗色色板:与浅色同色相的暖黑;white 语义为「输入 / 嵌入面」而非纯白 */ +export const darkTokens: ThemeTokens = { + ...tokens, + bg: '#191817', + card: '#201F1E', + white: '#262524', + ink: '#ECEAE4', + ink2: '#A8A69E', + ink3: '#807E76', + line: '#3A3835', + lineSoft: '#2C2B29', + rowHover: '#292825', + wash: '#2B2A27', + washDeep: '#33312D', + accent: '#D97757', + accentHover: '#E08B6D', + chart: '#D97757', + ok: '#8FA574', + err: '#D06060', + info: '#7FABD6', + warn: '#C29135', +} diff --git a/src/types/api.ts b/src/types/api.ts new file mode 100644 index 0000000..dd9e6e7 --- /dev/null +++ b/src/types/api.ts @@ -0,0 +1,969 @@ +/** + * 后端 DTO 类型,与《API接口文档.md》逐字段对应。 + * 网络三资源(VCN/Subnet/SecurityList)字段与 internal/oci/network.go 对齐。 + */ + +// ---- 认证 ---- +export interface LoginRequest { + username: string + password: string + /** 两步验证码;账号启用 TOTP 后必填,缺失时后端返回 428 */ + totpCode?: string +} + +export interface LoginResponse { + token: string + expiresAt: string +} + +/** 两步验证状态 */ +export interface TotpStatus { + enabled: boolean +} + +/** 两步验证设置返回:手动输入码与 otpauth URI(渲染二维码) */ +export interface TotpSetup { + secret: string + otpauthUri: string +} + +/** 已绑定的外部登录身份 */ +export interface UserIdentityInfo { + id: number + provider: string + display: string + createdAt: string +} + +/** 登录凭据摘要 */ +export interface CredentialsInfo { + username: string + /** 密码登录禁用态;开启需至少绑定一个外部身份 */ + passwordLoginDisabled: boolean +} + +/** 修改登录凭据;两项至少改一项,当前密码必填 */ +export interface UpdateCredentialsRequest { + newUsername?: string + newPassword?: string + currentPassword: string +} + +/** OAuth provider 配置视图;secret 只回设置态 */ +export interface OAuthSettings { + oidcIssuer: string + oidcClientId: string + oidcSecretSet: boolean + oidcDisplayName: string + oidcDisabled: boolean + githubClientId: string + githubSecretSet: boolean + githubDisplayName: string + githubDisabled: boolean +} + +/** 保存 provider 配置;secret 缺省沿用、空串清除 */ +export interface UpdateOAuthRequest { + oidcIssuer: string + oidcClientId: string + oidcClientSecret?: string + oidcDisplayName: string + oidcDisabled: boolean + githubClientId: string + githubClientSecret?: string + githubDisplayName: string + githubDisabled: boolean +} + +/** 登录页公开的 provider 信息;displayName 空配置时后端已回默认名 */ +export interface OauthProviderInfo { + provider: string + displayName: string +} + +// ---- API Key 配置(租户)---- +export type AccountType = 'paid' | 'trial' | 'free' | 'unknown' +export type AliveStatus = 'alive' | 'dead' | 'unknown' + +/** 列表接口(GET /oci-configs)的瘦身摘要:只含列表页与全局选择器消费的字段 */ +export interface OciConfigSummary { + id: number + alias: string + /** 面板内自定义分组,空串表示未分组 */ + group: string + tenancyOcid: string + tenancyName: string + region: string + accountType: AccountType + aliveStatus: AliveStatus + lastError: string + lastVerifiedAt: string | null + /** 多区域支持:开启后订阅区域入库缓存,筛选器可切换区域 */ + multiRegion: boolean + /** 多区间支持:开启后 compartment 入库缓存,筛选器可切换区间 */ + multiCompartment: boolean + /** 关联出站代理 id,null 直连 */ + proxyId: number | null + /** 关联代理名称,列表接口按关联填充 */ + proxyName: string + createdAt: string +} + +/** 详情接口(GET /oci-configs/:id)的完整快照,订阅 / 促销 / 凭据元数据只在这里返回 */ +export interface OciConfig extends OciConfigSummary { + userOcid: string + fingerprint: string + homeRegionKey: string + subscriptionId: string + paymentModel: string + subscriptionTier: string + promotionStatus: string + promotionAmount: number + promotionExpires: string | null + updatedAt: string +} + +export interface ImportConfigRequest { + alias: string + group?: string + privateKey: string + configIni?: string + tenancyOcid?: string + userOcid?: string + region?: string + fingerprint?: string + multiRegion?: boolean + multiCompartment?: boolean + passphrase?: string + /** 关联出站代理,缺省直连 */ + proxyId?: number +} + +export interface VerifyResult { + config: OciConfig + changes: Record +} + +/** 修改配置:空字段不变更;替换私钥须同时给新指纹;group 一经提供整体覆盖(空串取消分组) */ +export interface UpdateConfigRequest { + alias?: string + group?: string + region?: string + fingerprint?: string + privateKey?: string + passphrase?: string + multiRegion?: boolean + multiCompartment?: boolean + /** 非 null 切换代理关联:0 解除,>0 关联到该代理 */ + proxyId?: number +} + +// ---- 出站代理 ---- +export interface ProxyInfo { + id: number + name: string + /** socks5 / http / https */ + type: string + host: string + port: number + username: string + /** 密码已配置(不回明文) */ + passwordSet: boolean + /** 出口实测国家 / 城市;探测失败为空串 */ + country: string + city: string + /** 最近一次出口地理探测时间;空串表示尚未探测(显示「检测中」) */ + geoAt: string + /** 关联此代理的租户数 */ + usedBy: number + createdAt: string +} + +/** 创建 / 更新代理;name 留空自动生成,password 缺省沿用已存值,空串清除 */ +export interface ProxyInput { + name?: string + type: string + host: string + port: number + username?: string + password?: string +} + +/** 批量导入结果;失败行已脱敏(凭据段为 ***) */ +export interface ProxyImportResult { + created: ProxyInfo[] + failed: { lineNo: number; line: string; reason: string }[] +} + +// ---- 区域 ---- +export interface RegionInfo { + alias: string + key: string + name: string +} + +export interface RegionSubscription { + key: string + name: string + status: string + isHomeRegion: boolean + alias: string +} + +// ---- 配额 ---- +export interface LimitItem { + name: string + scopeType: string + availabilityDomain?: string + value: number + used?: number + available?: number +} + +export interface LimitService { + name: string + description: string +} + +// ---- 订阅 ---- +export interface SubscriptionSummary { + id: string + serviceName: string + timeCreated: string + timeUpdated: string +} + +export interface Promotion { + duration: number + durationUnit: string + amount: number + status: string + isIntentToPay: boolean + currencyUnit: string + timeStarted: string | null + timeExpired: string | null +} + +export interface SubscriptionDetail { + id: string + serviceName: string + classicSubscriptionId: string + paymentModel: string + subscriptionTier: string + lifecycleState: string + programType: string + customerCountryCode: string + cloudAmountCurrency: string + csiNumber: string + regionAssignment: string + isGovernmentSubscription: boolean + promotions: Promotion[] + startDate: string | null + endDate: string | null + timeCreated: string + timeUpdated: string +} + +// ---- 镜像与可用域 ---- +export interface ImageInfo { + id: string + displayName: string + operatingSystem: string + operatingSystemVersion: string + sizeInMBs: number + timeCreated: string +} + +// ---- 网络 ---- +export interface Vcn { + id: string + displayName: string + lifecycleState: string + compartmentId: string + cidrBlocks: string[] + ipv6CidrBlocks: string[] + dnsLabel: string + defaultRouteTableId: string + defaultSecurityListId: string + timeCreated: string | null +} + +export interface CreateVcnRequest { + region?: string + /** 目标 compartment,空为租户根 */ + compartmentId?: string + displayName: string + cidrBlock: string + dnsLabel?: string + enableIpv6?: boolean +} + +export interface Subnet { + id: string + vcnId: string + displayName: string + lifecycleState: string + cidrBlock: string + ipv6CidrBlock: string + dnsLabel: string + prohibitPublicIp: boolean + availabilityDomain: string + routeTableId: string + timeCreated: string | null +} + +export interface SecurityRule { + protocol: string + source?: string + destination?: string + isStateless: boolean + portMin?: number + portMax?: number + icmpType?: number + icmpCode?: number + description?: string +} + +export interface SecurityList { + id: string + vcnId: string + displayName: string + lifecycleState: string + ingressRules: SecurityRule[] + egressRules: SecurityRule[] + timeCreated: string | null +} + +export interface Ipv6Step { + step: string + status: 'done' | 'skipped' | 'failed' + detail: string +} + +export interface Ipv6Report { + steps: Ipv6Step[] +} + +// ---- 实例 ---- +export interface Instance { + id: string + displayName: string + lifecycleState: string + shape: string + ocpus: number + memoryInGBs: number + availabilityDomain: string + region: string + compartmentId: string + imageId: string + subnetId: string + privateIp: string + publicIp: string + ipv6Addresses: string[] + freeformTags?: Record + definedTags?: Record> + timeCreated: string +} + +export interface CreateInstanceRequest { + region?: string + /** 目标 compartment,空为租户根 */ + compartmentId?: string + availabilityDomain?: string + displayName: string + shape: string + count?: number + ocpus?: number + memoryInGBs?: number + imageId?: string + bootVolumeId?: string + bootVolumeSizeGBs?: number + bootVolumeVpusPerGB?: number + subnetId?: string + assignPublicIp?: boolean + assignIpv6?: boolean + sshPublicKey?: string + rootPassword?: string + /** 服务端生成随机 root 密码并写入 TAG RootPassword */ + generateRootPassword?: boolean + userData?: string +} + +export interface CreateInstancesResponse { + /** 旧版后端空值序列化为 null,读取时判空 */ + instances: Instance[] | null + errors: string[] | null +} + +export type PowerAction = 'START' | 'STOP' | 'RESET' | 'SOFTSTOP' | 'SOFTRESET' + +export interface UpdateInstanceRequest { + region?: string + displayName?: string + shape?: string + ocpus?: number + memoryInGBs?: number +} + +export interface ConsoleConnection { + id: string + instanceId: string + lifecycleState: string + connectionString: string + vncConnectionString: string +} + +export interface BootVolumeAttachment { + id: string + instanceId: string + bootVolumeId: string + lifecycleState: string + displayName: string + availabilityDomain: string +} + +export interface VolumeAttachment { + id: string + instanceId: string + volumeId: string + /** 卷本身的名称(displayName 是自动生成的挂载记录名) */ + volumeName?: string + lifecycleState: string + displayName: string + device: string + isReadOnly: boolean + availabilityDomain: string +} + +// ---- VNIC ---- +export interface Vnic { + attachmentId: string + vnicId: string + displayName: string + isPrimary: boolean + privateIp: string + publicIp: string + /** 后端 nil slice 序列化为 null,使用处需兜底空数组 */ + ipv6Addresses: string[] | null + subnetId: string + subnetName?: string + lifecycleState: string + timeCreated: string | null +} + +export interface AttachVnicRequest { + region?: string + subnetId: string + displayName?: string + privateIp?: string + assignPublicIp?: boolean + /** 附加时同步自动分配一个 IPv6,要求子网已启用 IPv6 */ + assignIpv6?: boolean +} + +// ---- 块卷 ---- +export interface Volume { + id: string + displayName: string + lifecycleState: string + availabilityDomain: string + sizeInGBs: number + vpusPerGB: number + timeCreated: string +} + +// ---- 实例规格 ---- +export interface ComputeShape { + name: string + billingType: string // ALWAYS_FREE / LIMITED_FREE / PAID + isFlexible: boolean + ocpus?: number + memoryInGBs?: number + ocpusMin?: number + ocpusMax?: number + memoryMinGBs?: number + memoryMaxGBs?: number + gpus?: number + processorDescription?: string +} + +// ---- 引导卷 ---- +export interface BootVolume { + id: string + displayName: string + lifecycleState: string + availabilityDomain: string + sizeInGBs: number + vpusPerGB: number + imageId: string + imageName?: string + attachedInstanceId: string + /** 挂载目标实例名(引导卷作数据卷挂到别的实例时,卷名与目标实例名不同) */ + attachedInstanceName?: string + timeCreated: string +} + +export interface UpdateBootVolumeRequest { + region?: string + displayName?: string + sizeInGBs?: number + vpusPerGB?: number +} + +// ---- 流量与成本 ---- +export interface TrafficPoint { + timestamp: string + bytes: number +} + +export interface VnicTraffic { + vnicId: string + inbound: TrafficPoint[] + outbound: TrafficPoint[] +} + +export interface InstanceTraffic { + instanceId: string + inboundBytes: number + outboundBytes: number + vnics: VnicTraffic[] +} + +export interface CostItem { + timeStart: string + groupValue: string + computedAmount: number + computedQuantity: number + currency: string + unit: string +} + +// ---- 租户用户管理 ---- +export interface IamUser { + id: string + name: string + description: string + email: string + emailVerified: boolean + mfaActivated: boolean + lifecycleState: string + isCurrentUser: boolean + timeCreated: string + lastLoginTime: string | null +} + +// 编辑表单用的域档案与管理员状态;inDomain 为 false 表示经典 IAM 用户 +export interface IamUserDetail { + inDomain: boolean + givenName: string + familyName: string + isDomainAdmin: boolean + inAdminGroup: boolean +} + +// ---- 租户审计日志 ---- +// OCI Audit 事件的列表精简视图;来自实时透传查询,不落库; +// 原始 JSON 不随列表返回,详情弹窗按 eventId 懒取 +export interface AuditEvent { + /** CloudEvents 全局唯一 id,详情反查与追加去重的键 */ + eventId: string + eventTime: string | null + eventName: string + source: string + resourceName: string + compartmentName: string + principalName: string + ipAddress: string + status: string + requestAction: string + requestPath: string +} + +// truncated 为 true 表示服务端翻页到限(10 页)被截断;nextPage 为续查游标, +// 配合 start/end 绝对时间窗可断点加载更早的事件 +export interface AuditEventsResult { + items: AuditEvent[] + truncated: boolean + nextPage?: string + /** 本次实际查询的绝对窗(RFC3339),续查必须原样带回 */ + start: string + end: string +} + +// ---- 域通知与密码策略 ---- +export interface NotificationRecipients { + recipients: string[] + testModeEnabled: boolean +} + +export interface PasswordPolicy { + id: string + name: string + priority: number + passwordStrength: string + passwordExpiresAfter?: number + minLength?: number + numPasswordsInHistory?: number +} + +// 身份设置:用户是否必须提供主电子邮件地址 +export interface IdentitySetting { + id: string + primaryEmailRequired: boolean +} + +// ---- Federation ---- +export interface IdentityProvider { + id: string + name: string + type: string + enabled: boolean + partnerProviderId: string + jitEnabled: boolean + timeCreated: string +} + +export interface SignOnRule { + id: string + name: string + sequence: number + authenticationFactor: string + conditionAttribute: string + conditionValue: string + builtIn: boolean +} + +// ---- Compartment ---- +export interface Compartment { + id: string + name: string + description: string + parentId: string + lifecycleState: string + timeCreated: string | null +} + +// ---- 总览 ---- +export interface OverviewTenants { + total: number + alive: number + dead: number + byType: Record +} + +export interface OverviewCheck { + hasActiveTask: boolean + coveredConfigs: number + totalConfigs: number + instanceCount: number + lastCheckedAt: string | null +} + +export interface OverviewCostDay { + day: string + amount: number +} + +export interface OverviewCost { + hasActiveTask: boolean + coveredConfigs: number + currency: string + total: number + days: OverviewCostDay[] +} + +export interface OverviewTasks { + total: number + active: number + healthCheck: number + cost: number + snatch: number +} + +export interface Overview { + tenants: OverviewTenants + check: OverviewCheck + cost: OverviewCost + tasks: OverviewTasks +} + +// ---- 后台任务 ---- +export type TaskType = 'health_check' | 'snatch' | 'cost' | 'ai_probe' +export type TaskStatus = 'active' | 'paused' | 'succeeded' | 'failed' + +export interface Task { + id: number + name: string + type: TaskType + cronExpr: string + payload: string + status: TaskStatus + lastRunAt: string | null + lastError: string + runCount: number + createdAt: string + updatedAt: string +} + +export interface TaskLog { + id: number + taskId: number + success: boolean + message: string + durationMs: number + createdAt: string +} + +// ---- 系统设置 ---- +export interface TelegramSetting { + enabled: boolean + chatId: string + /** 是否已保存过 bot token(接口绝不回明文) */ + tokenSet: boolean + /** 已存 token 尾 4 位,供占位提示 */ + tokenTail: string +} + +/** 通知事件开关(通知管理);存量部署未保存过时后端按全开返回 */ +export interface NotifyEventsSetting { + /** 任务由成功转失败时推送 */ + taskFail: boolean + /** 任务由失败恢复正常时推送 */ + taskRecover: boolean + /** 抢机任务达成目标台数时推送 */ + snatchSuccess: boolean + /** 测活失联集合变化时推送 */ + tenantDead: boolean + /** 抢机连续鉴权失败熔断停止时推送 */ + taskStop: boolean + /** 登录锁定(连续失败触发) */ + loginLock: boolean + /** AI 网关在池模型 30 天内弃用预警(随每日探测推送) */ + modelDeprecated: boolean + /** 云端事件·实例生命周期(Launch/Terminate/InstanceAction) */ + logEventInstance: boolean + /** 云端事件·用户与凭据(Create/Delete/UpdateUser、ApiKey、Capabilities) */ + logEventIdentity: boolean + /** 云端事件·策略变更(Create/Update/DeletePolicy) */ + logEventPolicy: boolean + /** 云端事件·区域订阅(CreateRegionSubscription) */ + logEventRegion: boolean + /** 云端事件·控制台登录(InteractiveLogin) */ + logEventLogin: boolean +} + +/** 通知模板(设置页「通知管理 → 模板」);template 空串表示未自定义(按默认发送) */ +export interface NotifyTemplateItem { + kind: string + label: string + /** 可用变量名(不含公共变量 time,发送时一并可用) */ + vars: string[] + defaultTemplate: string + template: string +} + +/** 安全设置(设置页「安全」tab):WAF 参数 / 真实IP请求头 / 面板地址 */ +export interface SecuritySetting { + /** 登录失败阈值(1-20):锁定时长窗口内失败达到该次数即锁定 */ + loginFailLimit: number + /** 登录锁定时长分钟(1-1440),同时是失败计数窗口 */ + loginLockMinutes: number + /** 全局每 IP 限速 req/s(1-100) */ + ipRateRps: number + /** 令牌桶突发额度(1-500) */ + ipRateBurst: number + /** 真实IP请求头:空=直连(RemoteAddr);反代后必须选择匹配的头 */ + realIpHeader: string + /** 面板公网基址,优先于 PUBLIC_URL 环境变量;回传链路与 OAuth 回调用 */ + appUrl: string +} + +/** 任务行为设置(设置页「任务」tab) */ +export interface TaskSetting { + /** 抢机连续 NotAuthenticated 熔断阈值,1-10,未保存过时后端回默认 3 */ + snatchAuthFailLimit: number +} + +// ---- 系统日志 ---- +/** 面板自身的操作留痕(写请求与登录成败),不含请求体 */ +export interface SystemLog { + id: number + /** 登录失败时为请求提交的用户名 */ + username: string + method: string + /** 路由模板(含 :id 占位符),登录为 /api/v1/auth/login */ + path: string + status: number + durationMs: number + clientIp: string + userAgent: string + /** 仅失败请求(>=400)有值:响应 error 文案 */ + errMsg: string + createdAt: string +} + +export interface SystemLogPage { + items: SystemLog[] + total: number +} + +// ---- 日志回传(方案A:OCI Connector Hub → Notifications → HTTPS 订阅) ---- +/** OCI 回传的日志事件;payload 为 ONS 消息原文,eventType 等由后台异步解析回填 */ +export interface LogEvent { + id: number + ociConfigId: number + /** ONS X-OCI-NS-MessageId,幂等键 */ + messageId: string + /** 如 com.oraclecloud.ComputeApi.TerminateInstance,解析前为空 */ + eventType: string + source: string + /** Audit 事件发起方 IP,解析回填,可能为空 */ + sourceIp: string + eventTime: string | null + payload: string + /** 超过面板 256KB 防御上限被截断 */ + truncated: boolean + processed: boolean + receivedAt: string +} + +export interface LogEventPage { + items: LogEvent[] + total: number +} + +/** 每租户回传回调地址;path 需以面板公网域名拼接完整 URL */ +export interface LogWebhookInfo { + path: string + secret: string + createdAt: string +} + +/** 回调地址查询结果:未生成时 exists 为 false 且无 webhook */ +export interface LogWebhookState { + exists: boolean + webhook?: LogWebhookInfo +} + +/** OCI 侧链路单资源状态;id 为空表示资源不存在 */ +export interface RelayResource { + id: string + state: string +} + +/** OCI 侧回传链路状态(P1 引导创建):Topic/订阅/Connector/Policy 一览 */ +export interface LogRelayView { + webhook?: LogWebhookInfo + /** 完整回调 URL;后端 PUBLIC_URL 已配置且 secret 已生成时返回 */ + endpoint?: string + topic: RelayResource + subscription: RelayResource + connector: RelayResource + policy: RelayResource + /** 全链路可用(四资源 ACTIVE 且 secret 有效) */ + ready: boolean + /** 关键事件清单:Connector Log Filter 白名单,命中才回传 */ + events: string[] +} + +// ---- AI 网关 ---- + +/** 网关密钥;明文不回显,仅 tail 尾 4 位。group 非空 = 只在同分组渠道内路由 */ +export interface AiKey { + id: number + name: string + tail: string + group: string + enabled: boolean + lastUsedAt: string | null + /** 内容日志开启截止时间;null = 永关(默认,红线),开启必须限时到期自动停写 */ + contentLogUntil: string | null + createdAt: string +} + +export type AiProbeStatus = '' | 'ok' | 'no_service' | 'no_quota' | 'error' + +/** 号池渠道 = 租户配置 × 区域 */ +export interface AiChannel { + id: number + name: string + ociConfigId: number + region: string + group: string + enabled: boolean + priority: number + weight: number + failCount: number + disabledUntil: string | null + lastProbeAt: string | null + probeStatus: AiProbeStatus + probeError: string + createdAt: string + updatedAt: string +} + +/** 聚合可用模型(OpenAI /models 形态) */ +export interface AiModel { + id: string + object: string + created: number + owned_by: string +} + +/** 渠道模型缓存行 */ +export interface AiModelCacheItem { + channelId: number + modelOcid: string + name: string + vendor: string + syncedAt: string +} + +/** 调用日志:仅元数据与 token 用量 */ +export interface AiCallLog { + id: number + keyId: number + keyName: string + endpoint: string + model: string + channelId: number + channelName: string + stream: boolean + status: number + /** 网关调用方来源 IP(尊重「真实 IP 头」安全设置) */ + clientIp: string + promptTokens: number + completionTokens: number + totalTokens: number + /** 输入中缓存命中(读取)的 token 数;OCI 隐式缓存无写入计数 */ + cachedTokens: number + latencyMs: number + retries: number + errMsg: string + createdAt: string +} + +/** 内容日志:「不落正文」红线的显式例外,密钥限时开启期间的调用正文快照 */ +export interface AiContentLog { + id: number + /** 关联同次调用的 AiCallLog id */ + callLogId: number + keyId: number + keyName: string + endpoint: string + model: string + stream: boolean + /** 截断后的正文 JSON(上限 64KB);流式与向量结果不记录响应正文 */ + requestBody: string + responseBody: string + createdAt: string +} + +// ---- 关于 ---- +/** GET /about:构建与运行环境信息 */ +export interface AboutInfo { + version: string + buildTime: string + goVersion: string + platform: string +} diff --git a/src/types/novnc.d.ts b/src/types/novnc.d.ts new file mode 100644 index 0000000..7296b99 --- /dev/null +++ b/src/types/novnc.d.ts @@ -0,0 +1,15 @@ +declare module '@novnc/novnc' { + /** noVNC RFB 客户端(官方包无类型声明,这里只声明用到的成员) */ + export default class RFB extends EventTarget { + constructor( + target: Element, + url: string, + options?: { credentials?: { username?: string; password?: string; target?: string } }, + ) + scaleViewport: boolean + resizeSession: boolean + background: string + disconnect(): void + sendCtrlAltDel(): void + } +} diff --git a/src/views/AiGatewayView.vue b/src/views/AiGatewayView.vue new file mode 100644 index 0000000..e80659a --- /dev/null +++ b/src/views/AiGatewayView.vue @@ -0,0 +1,181 @@ + + + diff --git a/src/views/BootVolumeListView.vue b/src/views/BootVolumeListView.vue new file mode 100644 index 0000000..e516a70 --- /dev/null +++ b/src/views/BootVolumeListView.vue @@ -0,0 +1,294 @@ + + + diff --git a/src/views/InstanceDetailView.vue b/src/views/InstanceDetailView.vue new file mode 100644 index 0000000..6464b6b --- /dev/null +++ b/src/views/InstanceDetailView.vue @@ -0,0 +1,766 @@ + + + diff --git a/src/views/InstanceListView.vue b/src/views/InstanceListView.vue new file mode 100644 index 0000000..d1e4974 --- /dev/null +++ b/src/views/InstanceListView.vue @@ -0,0 +1,229 @@ + + + diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue new file mode 100644 index 0000000..2da54b6 --- /dev/null +++ b/src/views/LoginView.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/src/views/LogsView.vue b/src/views/LogsView.vue new file mode 100644 index 0000000..a09a90b --- /dev/null +++ b/src/views/LogsView.vue @@ -0,0 +1,30 @@ + + + diff --git a/src/views/NetworkListView.vue b/src/views/NetworkListView.vue new file mode 100644 index 0000000..76b6a74 --- /dev/null +++ b/src/views/NetworkListView.vue @@ -0,0 +1,174 @@ + + + diff --git a/src/views/OverviewView.vue b/src/views/OverviewView.vue new file mode 100644 index 0000000..a8d2d4f --- /dev/null +++ b/src/views/OverviewView.vue @@ -0,0 +1,243 @@ + + + diff --git a/src/views/ProxyListView.vue b/src/views/ProxyListView.vue new file mode 100644 index 0000000..522a37c --- /dev/null +++ b/src/views/ProxyListView.vue @@ -0,0 +1,22 @@ + + + diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue new file mode 100644 index 0000000..52b203d --- /dev/null +++ b/src/views/SettingsView.vue @@ -0,0 +1,631 @@ + + + diff --git a/src/views/TaskDetailView.vue b/src/views/TaskDetailView.vue new file mode 100644 index 0000000..1efcffd --- /dev/null +++ b/src/views/TaskDetailView.vue @@ -0,0 +1,323 @@ + + + diff --git a/src/views/TaskListView.vue b/src/views/TaskListView.vue new file mode 100644 index 0000000..f9553d7 --- /dev/null +++ b/src/views/TaskListView.vue @@ -0,0 +1,113 @@ + + + diff --git a/src/views/TenantDetailView.vue b/src/views/TenantDetailView.vue new file mode 100644 index 0000000..7634112 --- /dev/null +++ b/src/views/TenantDetailView.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/src/views/TenantListView.vue b/src/views/TenantListView.vue new file mode 100644 index 0000000..927eac3 --- /dev/null +++ b/src/views/TenantListView.vue @@ -0,0 +1,331 @@ + + + diff --git a/src/views/VcnDetailView.vue b/src/views/VcnDetailView.vue new file mode 100644 index 0000000..9bb8df8 --- /dev/null +++ b/src/views/VcnDetailView.vue @@ -0,0 +1,308 @@ + + + diff --git a/src/views/VncConsoleView.vue b/src/views/VncConsoleView.vue new file mode 100644 index 0000000..baf6721 --- /dev/null +++ b/src/views/VncConsoleView.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/views/errors/BlockedView.vue b/src/views/errors/BlockedView.vue new file mode 100644 index 0000000..f233110 --- /dev/null +++ b/src/views/errors/BlockedView.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/views/errors/NotFoundView.vue b/src/views/errors/NotFoundView.vue new file mode 100644 index 0000000..16b08d9 --- /dev/null +++ b/src/views/errors/NotFoundView.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/src/views/errors/error-page.css b/src/views/errors/error-page.css new file mode 100644 index 0000000..aa6739a --- /dev/null +++ b/src/views/errors/error-page.css @@ -0,0 +1,70 @@ +/* 404 / 429 全屏错误页共享样式:状态徽章浮动 + 镂空巨字 */ +.chip { + display: inline-flex; + align-items: center; + gap: 7px; + border: 1px solid var(--color-line); + border-radius: 999px; + background: var(--color-card); + padding: 7px 14px; + font-size: 12px; + color: var(--color-ink-2); + box-shadow: 0 2px 8px rgba(20, 20, 19, 0.07); +} +.dot { + width: 7px; + height: 7px; + border-radius: 50%; +} +.bignum { + display: flex; + font-size: 250px; + font-weight: 700; + line-height: 1; + letter-spacing: 0.02em; +} +@media (max-width: 640px) { + .bignum { + font-size: 150px; + } +} +.hollow { + color: transparent; + -webkit-text-stroke: 2.5px var(--color-ink); +} +.meter { + display: flex; + height: 10px; + border-radius: 999px; + background: var(--color-wash-deep); + overflow: hidden; +} +@media (prefers-reduced-motion: no-preference) { + .floaty { + animation: err-fl 7s ease-in-out infinite; + } + .bob { + animation: err-bob 6.3s ease-in-out -2.1s infinite; + } + @keyframes err-fl { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } + } + @keyframes err-bob { + 0%, + 100% { + transform: translateY(0); + } + 32% { + transform: translateY(-8px); + } + 68% { + transform: translateY(3px); + } + } +} diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..f54ed27 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ba5ccc4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.app.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f4cb5d2 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": ["vite.config.ts"], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e879b9a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,41 @@ +import { fileURLToPath, URL } from 'node:url' + +import vue from '@vitejs/plugin-vue' +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + // noVNC 1.7 使用 top-level await,需较新的构建目标(现代浏览器均支持); + // 生产构建与 dev 依赖预构建(esbuild)都要提高 target,否则 dev 会报同一错误 + build: { + target: 'es2022', + }, + optimizeDeps: { + esbuildOptions: { + target: 'es2022', + }, + }, + server: { + port: 5173, + host: '127.0.0.1', + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + ws: true, + }, + // 只代理网关端点 /ai/*;裸 '/ai' 前缀会误拦 /ai-gateway 页面路由 + '^/ai/': { + target: 'http://localhost:8080', + changeOrigin: true, + ws: true, + }, + }, + }, +})