From eb827456384e476fd4069a6ca3017431cdc60cd0 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 29 Sep 2025 20:19:18 +0000 Subject: [PATCH] chore: tidy repo formatting and linting --- .github/workflows/ci.yml | 57 + .golangci.yml | 20 + Makefile | 20 +- cmd/pulse/config.go | 10 +- cmd/pulse/main.go | 48 +- frontend-modern/.eslintignore | 3 + frontend-modern/.eslintrc.cjs | 41 + frontend-modern/.prettierignore | 3 + frontend-modern/.prettierrc | 6 + frontend-modern/package-lock.json | 1431 +++++ frontend-modern/package.json | 12 +- frontend-modern/src/App.tsx | 703 ++- frontend-modern/src/SimpleApp.tsx | 30 +- frontend-modern/src/Test.tsx | 10 +- frontend-modern/src/api/alerts.ts | 13 +- frontend-modern/src/api/guestMetadata.ts | 8 +- frontend-modern/src/api/monitoring.ts | 2 +- frontend-modern/src/api/nodes.ts | 15 +- frontend-modern/src/api/notifications.ts | 30 +- frontend-modern/src/api/settings.ts | 11 +- frontend-modern/src/api/system.ts | 4 +- frontend-modern/src/api/updates.ts | 2 +- .../src/components/Alerts/CustomRulesTab.tsx | 262 +- .../components/Alerts/EmailProviderSelect.tsx | 36 +- .../src/components/Alerts/OverrideModal.tsx | 121 +- .../src/components/Alerts/ResourceTable.tsx | 919 +-- .../src/components/Alerts/ThresholdsTable.tsx | 1162 ++-- .../src/components/Alerts/WebhookConfig.tsx | 225 +- .../src/components/Backups/Backups.tsx | 44 +- .../src/components/Backups/BackupsFilter.tsx | 130 +- .../src/components/Backups/UnifiedBackups.tsx | 2367 ++++---- .../components/Dashboard/CompactNodeCard.tsx | 114 +- .../src/components/Dashboard/Dashboard.tsx | 779 ++- .../components/Dashboard/DashboardFilter.tsx | 94 +- .../Dashboard/DiskHealthSummary.tsx | 49 +- .../src/components/Dashboard/GuestRow.tsx | 119 +- .../src/components/Dashboard/IOMetric.tsx | 18 +- .../src/components/Dashboard/MetricBar.tsx | 14 +- .../src/components/Dashboard/NodeCard.tsx | 98 +- .../src/components/Dashboard/PBSCard.tsx | 122 +- .../src/components/Dashboard/TagBadges.tsx | 32 +- .../components/Dashboard/ThresholdSlider.tsx | 46 +- .../src/components/ErrorBoundary.tsx | 54 +- .../src/components/FirstRunSetup.tsx | 186 +- frontend-modern/src/components/Login.tsx | 206 +- .../src/components/NotificationContainer.tsx | 2 +- .../src/components/NotificationToast.tsx | 74 +- .../src/components/SecurityWarning.tsx | 63 +- .../src/components/Settings/APIOnlySetup.tsx | 48 +- .../Settings/BatchCredentialModal.tsx | 85 +- .../Settings/ChangePasswordModal.tsx | 38 +- .../components/Settings/DiscoveryModal.tsx | 160 +- .../components/Settings/GenerateAPIToken.tsx | 89 +- .../src/components/Settings/GuestURLs.tsx | 352 +- .../src/components/Settings/NodeModal.tsx | 1199 ++-- .../src/components/Settings/OIDCPanel.tsx | 76 +- .../Settings/QuickSecuritySetup.tsx | 174 +- .../Settings/SecurityPostureSummary.tsx | 40 +- .../src/components/Settings/Settings.tsx | 5188 ++++++++++------- .../src/components/Storage/DiskList.tsx | 237 +- .../src/components/Storage/Storage.tsx | 738 ++- .../src/components/Storage/StorageFilter.tsx | 80 +- .../src/components/Toast/Toast.tsx | 66 +- .../src/components/UpdateBanner.tsx | 76 +- .../src/components/shared/AlertIndicators.tsx | 37 +- .../src/components/shared/AnimatedMetric.tsx | 23 +- .../src/components/shared/Card.tsx | 9 +- .../src/components/shared/EmptyState.tsx | 32 +- frontend-modern/src/components/shared/Form.ts | 7 +- .../components/shared/NodeSummaryTable.tsx | 240 +- .../src/components/shared/ScrollableTable.tsx | 14 +- .../src/components/shared/SectionHeader.tsx | 27 +- .../src/components/shared/SettingsPanel.tsx | 15 +- .../src/components/shared/Toggle.tsx | 20 +- .../src/components/shared/Tooltip.tsx | 41 +- .../components/shared/UnifiedNodeSelector.tsx | 46 +- frontend-modern/src/constants.ts | 23 +- .../src/hooks/useIntersectionObserver.ts | 8 +- frontend-modern/src/index.css | 50 +- frontend-modern/src/index.tsx | 3 +- frontend-modern/src/pages/Alerts.tsx | 1477 +++-- frontend-modern/src/stores/events.ts | 23 +- frontend-modern/src/stores/notifications.ts | 22 +- frontend-modern/src/stores/updates.ts | 34 +- .../src/stores/websocket-global.ts | 6 +- frontend-modern/src/stores/websocket.ts | 520 +- frontend-modern/src/styles/animations.css | 6 +- frontend-modern/src/types/api.ts | 106 +- frontend-modern/src/types/config.ts | 58 +- frontend-modern/src/types/monitoring.ts | 2 +- frontend-modern/src/types/settings.ts | 2 +- frontend-modern/src/utils/alerts.ts | 35 +- frontend-modern/src/utils/apiClient.ts | 80 +- frontend-modern/src/utils/clipboard.ts | 2 +- frontend-modern/src/utils/errorHandler.ts | 11 +- frontend-modern/src/utils/format.ts | 37 +- frontend-modern/src/utils/localStorage.ts | 31 +- frontend-modern/src/utils/logger.ts | 17 +- frontend-modern/src/utils/searchQuery.ts | 137 +- frontend-modern/src/utils/tagColors.ts | 43 +- frontend-modern/src/utils/toast.ts | 5 +- frontend-modern/src/vite-env.d.ts | 2 +- internal/adapters/state_adapter.go | 2 +- internal/alerts/alerts.go | 588 +- internal/alerts/history.go | 14 +- internal/api/alerts.go | 132 +- internal/api/config_handlers.go | 13 +- internal/api/csrf_store.go | 2 +- internal/api/diagnostics.go | 270 +- internal/api/guest_metadata.go | 20 +- internal/api/middleware.go | 32 +- internal/api/notifications.go | 150 +- internal/api/rate_limit_config.go | 56 +- internal/api/ratelimit.go | 30 +- internal/api/recovery_tokens.go | 68 +- internal/api/router.go | 12 +- internal/api/security.go | 88 +- internal/api/security_setup_fix.go | 27 +- internal/api/session_store.go | 16 +- internal/api/settings.go | 28 +- internal/api/system_settings.go | 67 +- internal/api/types.go | 107 +- internal/api/updates.go | 10 +- internal/auth/password.go | 7 +- internal/auth/token.go | 3 +- internal/config/credentials.go | 13 +- internal/config/export.go | 20 +- internal/config/guest_metadata.go | 52 +- internal/config/registration.go | 20 +- internal/config/settings.go | 12 +- internal/config/watcher.go | 54 +- internal/crypto/crypto.go | 36 +- internal/discovery/service.go | 38 +- internal/errors/errors.go | 57 +- internal/interfaces/interfaces.go | 3 - internal/mock/alert_history.go | 50 +- internal/mock/generator.go | 432 +- internal/mock/integration.go | 38 +- internal/models/cluster.go | 14 +- internal/models/converters.go | 14 +- internal/models/models.go | 356 +- internal/models/models_frontend.go | 112 +- internal/models/state_snapshot.go | 54 +- internal/monitoring/metrics_history.go | 16 +- internal/monitoring/monitor.go | 835 +-- internal/monitoring/monitor_optimized.go | 438 +- internal/monitoring/poller.go | 48 +- internal/monitoring/ratetracker.go | 16 +- internal/monitoring/reload.go | 24 +- internal/notifications/email_enhanced.go | 125 +- internal/notifications/email_providers.go | 2 +- internal/notifications/email_template.go | 16 +- internal/notifications/notifications.go | 274 +- internal/notifications/webhook_enhanced.go | 110 +- internal/notifications/webhook_templates.go | 14 +- internal/types/metrics.go | 2 +- internal/updates/manager.go | 117 +- internal/updates/version.go | 59 +- internal/utils/command.go | 2 +- internal/utils/generics.go | 12 +- internal/utils/helpers.go | 3 +- internal/utils/network.go | 28 +- internal/utils/paths.go | 2 +- internal/utils/ratelimiter.go | 34 +- internal/websocket/hub.go | 93 +- pkg/discovery/discovery.go | 75 +- pkg/pbs/client.go | 167 +- pkg/proxmox/client.go | 303 +- pkg/proxmox/cluster_client.go | 210 +- pkg/proxmox/zfs.go | 31 +- pkg/tlsutil/fingerprint.go | 32 +- 171 files changed, 16834 insertions(+), 11083 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .golangci.yml create mode 100644 frontend-modern/.eslintignore create mode 100644 frontend-modern/.eslintrc.cjs create mode 100644 frontend-modern/.prettierignore create mode 100644 frontend-modern/.prettierrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..b15af6618 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +name: CI + +on: + push: + branches: + - main + - master + pull_request: + +jobs: + backend: + name: Backend + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Verify gofmt + run: | + fmt_out=$(gofmt -l ./cmd ./internal ./pkg) + if [ -n "$fmt_out" ]; then + echo "The following files are not gofmt formatted:" + echo "$fmt_out" + exit 1 + fi + - name: Go vet + run: go vet ./... + - name: Go test + run: go test ./... + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + args: ./... + + frontend: + name: Frontend + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend-modern + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: frontend-modern/package-lock.json + - name: Install dependencies + run: npm ci + - name: Lint + run: npm run lint + - name: Type check + run: npm run type-check + - name: Format check + run: npm run format:check diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..34d1d4a34 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,20 @@ +run: + timeout: 5m + tests: true +linters: + disable-all: true + enable: + - govet + - gofmt + - goimports + - errcheck +issues: + max-same-issues: 0 + max-issues-per-linter: 0 +linters-settings: + gofmt: + simplify: true + errcheck: + exclude-functions: + - (*encoding/json.Encoder).Encode + - (net/http.ResponseWriter).Write diff --git a/Makefile b/Makefile index 2b088ab09..842526f08 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Pulse Makefile for development -.PHONY: build run dev frontend backend all clean dev-hot +.PHONY: build run dev frontend backend all clean dev-hot lint lint-backend lint-frontend format format-backend format-frontend # Build everything all: frontend backend @@ -43,3 +43,21 @@ clean: # Quick rebuild and restart for development restart: frontend backend sudo systemctl restart pulse-backend + +# Run linters for both backend and frontend +lint: lint-backend lint-frontend + +lint-backend: + golangci-lint run ./... + +lint-frontend: + cd frontend-modern && npm run lint + +# Apply formatters +format: format-backend format-frontend + +format-backend: + gofmt -w cmd internal pkg + +format-frontend: + cd frontend-modern && npm run format diff --git a/cmd/pulse/config.go b/cmd/pulse/config.go index 1fee3b146..0f64bae81 100644 --- a/cmd/pulse/config.go +++ b/cmd/pulse/config.go @@ -15,10 +15,10 @@ import ( ) var ( - exportFile string - importFile string - passphrase string - forceImport bool + exportFile string + importFile string + passphrase string + forceImport bool ) var configCmd = &cobra.Command{ @@ -267,4 +267,4 @@ func init() { configImportCmd.Flags().StringVarP(&importFile, "input", "i", "", "Input file with encrypted configuration") configImportCmd.Flags().StringVarP(&passphrase, "passphrase", "p", "", "Passphrase for decryption (or use PULSE_PASSPHRASE env var)") configImportCmd.Flags().BoolVarP(&forceImport, "force", "f", false, "Force import without confirmation") -} \ No newline at end of file +} diff --git a/cmd/pulse/main.go b/cmd/pulse/main.go index 78892793a..2d231f8f9 100644 --- a/cmd/pulse/main.go +++ b/cmd/pulse/main.go @@ -30,9 +30,9 @@ var ( ) var rootCmd = &cobra.Command{ - Use: "pulse", - Short: "Pulse - Proxmox VE and PBS monitoring system", - Long: `Pulse is a real-time monitoring system for Proxmox Virtual Environment (PVE) and Proxmox Backup Server (PBS)`, + Use: "pulse", + Short: "Pulse - Proxmox VE and PBS monitoring system", + Long: `Pulse is a real-time monitoring system for Proxmox Virtual Environment (PVE) and Proxmox Backup Server (PBS)`, Version: Version, Run: func(cmd *cobra.Command, args []string) { runServer() @@ -151,7 +151,7 @@ func runServer() { } defer configWatcher.Stop() } - + // Start server go func() { if cfg.HTTPSEnabled && cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { @@ -181,23 +181,23 @@ func runServer() { // Setup signal handlers sigChan := make(chan os.Signal, 1) reloadChan := make(chan os.Signal, 1) - + // SIGTERM and SIGINT for shutdown signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) // SIGHUP for config reload signal.Notify(reloadChan, syscall.SIGHUP) - + // Handle signals for { select { case <-reloadChan: log.Info().Msg("Received SIGHUP, reloading configuration...") - + // Reload .env manually (watcher will also pick it up) if configWatcher != nil { configWatcher.ReloadConfig() } - + // Reload system.json persistence := config.NewConfigPersistence(cfg.DataPath) if persistence != nil { @@ -210,17 +210,17 @@ func runServer() { log.Error().Err(err).Msg("Failed to reload system.json") } } - + // Could reload other configs here (alerts.json, webhooks.json, etc.) - + log.Info().Msg("Configuration reload complete") - + case <-sigChan: log.Info().Msg("Shutting down server...") goto shutdown } } - + shutdown: // Graceful shutdown @@ -234,7 +234,7 @@ shutdown: // Stop monitoring cancel() reloadableMonitor.Stop() - + // Stop config watcher if configWatcher != nil { configWatcher.Stop() @@ -250,15 +250,15 @@ func shouldAutoImport() bool { if configPath == "" { configPath = "/etc/pulse" } - + // If nodes.enc already exists, skip auto-import if _, err := os.Stat(filepath.Join(configPath, "nodes.enc")); err == nil { return false } - + // Check for auto-import environment variables - return os.Getenv("PULSE_INIT_CONFIG_DATA") != "" || - os.Getenv("PULSE_INIT_CONFIG_FILE") != "" + return os.Getenv("PULSE_INIT_CONFIG_DATA") != "" || + os.Getenv("PULSE_INIT_CONFIG_FILE") != "" } // performAutoImport imports configuration from environment variables @@ -266,13 +266,13 @@ func performAutoImport() error { configData := os.Getenv("PULSE_INIT_CONFIG_DATA") configFile := os.Getenv("PULSE_INIT_CONFIG_FILE") configPass := os.Getenv("PULSE_INIT_CONFIG_PASSPHRASE") - + if configPass == "" { return fmt.Errorf("PULSE_INIT_CONFIG_PASSPHRASE is required for auto-import") } - + var encryptedData string - + // Get data from file or direct data if configFile != "" { data, err := os.ReadFile(configFile) @@ -290,21 +290,21 @@ func performAutoImport() error { } else { return fmt.Errorf("no config data provided") } - + // Load configuration path configPath := os.Getenv("PULSE_DATA_DIR") if configPath == "" { configPath = "/etc/pulse" } - + // Create persistence manager persistence := config.NewConfigPersistence(configPath) - + // Import configuration if err := persistence.ImportConfig(encryptedData, configPass); err != nil { return fmt.Errorf("failed to import configuration: %w", err) } - + log.Info().Msg("Configuration auto-imported successfully") return nil } diff --git a/frontend-modern/.eslintignore b/frontend-modern/.eslintignore new file mode 100644 index 000000000..e6a82761f --- /dev/null +++ b/frontend-modern/.eslintignore @@ -0,0 +1,3 @@ +dist +node_modules +public diff --git a/frontend-modern/.eslintrc.cjs b/frontend-modern/.eslintrc.cjs new file mode 100644 index 000000000..e6c22c717 --- /dev/null +++ b/frontend-modern/.eslintrc.cjs @@ -0,0 +1,41 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:solid/typescript', + 'prettier', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'solid'], + ignorePatterns: ['dist', 'node_modules'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'no-case-declarations': 'off', + 'no-useless-escape': 'off', + 'prefer-const': 'off', + 'solid/reactivity': 'off', + 'solid/prefer-for': 'off', + 'solid/style-prop': 'off', + 'solid/components-return-once': 'off', + 'solid/self-closing-comp': 'off', + }, +}; diff --git a/frontend-modern/.prettierignore b/frontend-modern/.prettierignore new file mode 100644 index 000000000..e6a82761f --- /dev/null +++ b/frontend-modern/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +public diff --git a/frontend-modern/.prettierrc b/frontend-modern/.prettierrc new file mode 100644 index 000000000..9014aaa17 --- /dev/null +++ b/frontend-modern/.prettierrc @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 100 +} diff --git a/frontend-modern/package-lock.json b/frontend-modern/package-lock.json index a3947e959..4a2e6d1de 100644 --- a/frontend-modern/package-lock.json +++ b/frontend-modern/package-lock.json @@ -15,8 +15,14 @@ }, "devDependencies": { "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "autoprefixer": "^10.4.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-solid": "^0.14.0", "postcss": "^8.4.0", + "prettier": "^3.3.0", "tailwindcss": "^3.4.0", "typescript": "^5.3.0", "vite": "^6.3.5", @@ -721,6 +727,165 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@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/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1142,6 +1307,301 @@ "undici-types": "~6.21.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", + "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/type-utils": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.45.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", + "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", + "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.45.0", + "@typescript-eslint/types": "^8.45.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", + "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", + "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", + "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", + "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", + "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.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", + "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.45.0", + "@typescript-eslint/tsconfig-utils": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", + "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", + "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/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/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -1191,6 +1651,13 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "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/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -1340,6 +1807,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1369,6 +1846,39 @@ } ] }, + "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/chalk/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/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1432,6 +1942,13 @@ "node": ">= 6" } }, + "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/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1486,6 +2003,13 @@ } } }, + "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/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1498,6 +2022,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1578,6 +2115,269 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-solid": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-solid/-/eslint-plugin-solid-0.14.5.tgz", + "integrity": "sha512-nfuYK09ah5aJG/oEN6P1qziy1zLgW4PDWe75VNPi4CEFYk1x2AEqwFeQfEPR7gNn0F2jOeqKhx2E+5oNCOBYWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^7.13.1 || ^8.0.0", + "estraverse": "^5.3.0", + "is-html": "^2.0.0", + "kebab-case": "^1.0.2", + "known-css-properties": "^0.30.0", + "style-to-object": "^1.0.6" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=4.8.4" + } + }, + "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-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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/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/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/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/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-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -1606,6 +2406,20 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -1615,6 +2429,19 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1627,6 +2454,45 @@ "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": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -1656,6 +2522,13 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1720,6 +2593,39 @@ "node": ">=10.13.0" } }, + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -1738,6 +2644,82 @@ "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", "dev": true }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1795,6 +2777,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-html": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-html/-/is-html-2.0.0.tgz", + "integrity": "sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "html-tags": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1804,6 +2799,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-what": { "version": "4.1.16", "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", @@ -1852,6 +2857,19 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -1864,6 +2882,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -1876,6 +2915,44 @@ "node": ">=6" } }, + "node_modules/kebab-case": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", + "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==", + "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/known-css-properties": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.30.0.tgz", + "integrity": "sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -1894,6 +2971,29 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -1996,6 +3096,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -2038,12 +3145,85 @@ "node": ">= 6" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "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/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -2056,6 +3236,26 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2266,6 +3466,42 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "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", @@ -2327,6 +3563,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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", @@ -2337,6 +3583,69 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", @@ -2589,6 +3898,29 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -2611,6 +3943,19 @@ "node": ">=16 || 14 >=14.17" } }, + "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/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2660,6 +4005,13 @@ "node": ">=14.0.0" } }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2735,12 +4087,51 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "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.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", @@ -2790,6 +4181,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2955,6 +4356,16 @@ "node": ">= 8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -3046,6 +4457,13 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3083,6 +4501,19 @@ "engines": { "node": ">= 14.6" } + }, + "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" + } } } } diff --git a/frontend-modern/package.json b/frontend-modern/package.json index 01ab7c300..519d64ff4 100644 --- a/frontend-modern/package.json +++ b/frontend-modern/package.json @@ -18,7 +18,11 @@ "build": "vite build", "preview": "vite preview", "generate-types": "cd ../scripts && go run generate-types.go", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "lint": "eslint \"src/**/*.{ts,tsx}\"", + "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"" }, "dependencies": { "@solidjs/router": "^0.10.0", @@ -27,8 +31,14 @@ }, "devDependencies": { "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "autoprefixer": "^10.4.0", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-solid": "^0.14.0", "postcss": "^8.4.0", + "prettier": "^3.3.0", "tailwindcss": "^3.4.0", "typescript": "^5.3.0", "vite": "^6.3.5", diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index eb8ab1b54..bcf09a8a1 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -1,4 +1,14 @@ -import { Show, createSignal, createContext, useContext, createEffect, onMount, onCleanup, getOwner, runWithOwner } from 'solid-js'; +import { + Show, + createSignal, + createContext, + useContext, + createEffect, + onMount, + onCleanup, + getOwner, + runWithOwner, +} from 'solid-js'; import { getGlobalWebSocketStore } from './stores/websocket-global'; import { Dashboard } from './components/Dashboard/Dashboard'; import StorageComponent from './components/Storage/Storage'; @@ -48,7 +58,9 @@ export const useDarkMode = () => { function App() { const owner = getOwner(); const acquireWsStore = (): EnhancedStore => { - const store = owner ? runWithOwner(owner, () => getGlobalWebSocketStore()) : getGlobalWebSocketStore(); + const store = owner + ? runWithOwner(owner, () => getGlobalWebSocketStore()) + : getGlobalWebSocketStore(); return store || getGlobalWebSocketStore(); }; @@ -56,18 +68,22 @@ function App() { const [isLoading, setIsLoading] = createSignal(true); const [needsAuth, setNeedsAuth] = createSignal(false); const [hasAuth, setHasAuth] = createSignal(false); - const [proxyAuthInfo, setProxyAuthInfo] = createSignal<{ username?: string; logoutURL?: string } | null>(null); - + const [proxyAuthInfo, setProxyAuthInfo] = createSignal<{ + username?: string; + logoutURL?: string; + } | null>(null); + // Don't initialize WebSocket until after auth check const [wsStore, setWsStore] = createSignal(null); - const state = () => wsStore()?.state || { vms: [], containers: [], nodes: [], pbs: [], lastUpdate: '' }; + const state = () => + wsStore()?.state || { vms: [], containers: [], nodes: [], pbs: [], lastUpdate: '' }; const connected = () => wsStore()?.connected() || false; const reconnecting = () => wsStore()?.reconnecting() || false; - + // Data update indicator const [dataUpdated, setDataUpdated] = createSignal(false); let updateTimeout: number; - + // Flash indicator when data updates createEffect(() => { // Watch for state changes @@ -78,22 +94,24 @@ function App() { updateTimeout = window.setTimeout(() => setDataUpdated(false), POLLING_INTERVALS.DATA_FLASH); } }); - + // Tab management with localStorage persistence const savedTab = localStorage.getItem(STORAGE_KEYS.ACTIVE_TAB) as TabType; const [activeTab, setActiveTab] = createSignal( - savedTab && ['main', 'storage', 'backups', 'alerts', 'settings'].includes(savedTab) ? savedTab : 'main' + savedTab && ['main', 'storage', 'backups', 'alerts', 'settings'].includes(savedTab) + ? savedTab + : 'main', ); - + // Persist tab selection const changeTab = (tab: TabType) => { setActiveTab(tab); localStorage.setItem(STORAGE_KEYS.ACTIVE_TAB, tab); }; - + // Version info const [versionInfo, setVersionInfo] = createSignal(null); - + // Dark mode - initialize immediately from localStorage to prevent flash // This addresses issue #443 where dark mode wasn't persisting // Priority: 1. localStorage (user's last choice on this device) @@ -106,14 +124,14 @@ function App() { : window.matchMedia('(prefers-color-scheme: dark)').matches; const [darkMode, setDarkMode] = createSignal(initialDarkMode); const [, setHasLoadedServerTheme] = createSignal(false); - + // Apply dark mode immediately on initialization if (initialDarkMode) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } - + // Toggle dark mode const toggleDarkMode = async () => { const newMode = !darkMode(); @@ -125,7 +143,7 @@ function App() { document.documentElement.classList.remove('dark'); } logger.info('Theme changed', { mode: newMode ? 'dark' : 'light' }); - + // Save theme preference to server if authenticated if (!needsAuth()) { try { @@ -137,20 +155,20 @@ function App() { } } }; - + // Don't initialize dark mode here - will be handled based on auth state - + // Listen for theme changes from other browser instances onMount(() => { const handleThemeChange = (theme?: string) => { if (!theme) return; logger.info('Received theme change from another browser instance', { theme }); const isDark = theme === 'dark'; - + // Update local state setDarkMode(isDark); localStorage.setItem(STORAGE_KEYS.DARK_MODE, String(isDark)); - + // Update DOM if (isDark) { document.documentElement.classList.add('dark'); @@ -158,20 +176,20 @@ function App() { document.documentElement.classList.remove('dark'); } }; - + // Subscribe to theme change events eventBus.on('theme_changed', handleThemeChange); - + // Cleanup on unmount onCleanup(() => { eventBus.off('theme_changed', handleThemeChange); }); }); - + // Check auth on mount onMount(async () => { console.log('[App] Starting auth check...'); - + // Check if we just logged out - if so, always show login page const justLoggedOut = localStorage.getItem('just_logged_out'); if (justLoggedOut) { @@ -182,13 +200,13 @@ function App() { setIsLoading(false); return; } - + // First check security status to see if auth is configured try { const securityRes = await apiFetch('/api/security/status'); const securityData = await securityRes.json(); console.log('[App] Security status:', securityData); - + // Check if auth is disabled via DISABLE_AUTH if (securityData.disabled === true) { console.log('[App] Auth is disabled via DISABLE_AUTH, skipping authentication'); @@ -196,7 +214,7 @@ function App() { setNeedsAuth(false); // Initialize WebSocket immediately since no auth needed setWsStore(acquireWsStore()); - + // Load theme preference from server for cross-device sync // Only use server preference if no local preference exists if (!hasLocalPreference) { @@ -217,34 +235,34 @@ function App() { console.error('Failed to load theme from server:', error); } } - + // Load version info even when auth is disabled UpdatesAPI.getVersion() - .then(version => { + .then((version) => { setVersionInfo(version); // Check for updates after loading version info (non-blocking) updateStore.checkForUpdates(); }) - .catch(error => console.error('Failed to load version:', error)); - + .catch((error) => console.error('Failed to load version:', error)); + setIsLoading(false); return; } - + const authConfigured = securityData.hasAuthentication || false; setHasAuth(authConfigured); - + // Check for proxy auth if (securityData.hasProxyAuth && securityData.proxyAuthUsername) { console.log('[App] Proxy auth detected, user:', securityData.proxyAuthUsername); setProxyAuthInfo({ username: securityData.proxyAuthUsername, - logoutURL: securityData.proxyAuthLogoutURL + logoutURL: securityData.proxyAuthLogoutURL, }); setNeedsAuth(false); // Initialize WebSocket for proxy auth users setWsStore(acquireWsStore()); - + // Load theme preference from server for cross-device sync // Only use server preference if no local preference exists if (!hasLocalPreference) { @@ -265,20 +283,20 @@ function App() { console.error('Failed to load theme from server:', error); } } - + // Load version info UpdatesAPI.getVersion() - .then(version => { + .then((version) => { setVersionInfo(version); // Check for updates after loading version info (non-blocking) updateStore.checkForUpdates(); }) - .catch(error => console.error('Failed to load version:', error)); - + .catch((error) => console.error('Failed to load version:', error)); + setIsLoading(false); return; } - + // If no auth is configured, show FirstRunSetup if (!authConfigured) { console.log('[App] No auth configured, showing Login/FirstRunSetup'); @@ -286,22 +304,22 @@ function App() { setIsLoading(false); return; } - + // If auth is configured, check if we're authenticated const stateRes = await apiFetch('/api/state', { headers: { 'X-Requested-With': 'XMLHttpRequest', - 'Accept': 'application/json' - } + Accept: 'application/json', + }, }); - + if (stateRes.status === 401) { setNeedsAuth(true); } else { setNeedsAuth(false); // Only initialize WebSocket after successful auth check setWsStore(acquireWsStore()); - + // Load theme preference from server for cross-device sync // Only use server preference if no local preference exists if (!hasLocalPreference) { @@ -331,26 +349,26 @@ function App() { // On error, try to proceed without auth setNeedsAuth(false); setWsStore(acquireWsStore()); - + // Theme is already applied on initialization, no need to reapply } finally { setIsLoading(false); } - + // Load version info UpdatesAPI.getVersion() - .then(version => { + .then((version) => { setVersionInfo(version); // Check for updates after loading version info (non-blocking) updateStore.checkForUpdates(); }) - .catch(error => console.error('Failed to load version:', error)); + .catch((error) => console.error('Failed to load version:', error)); }); - + const handleLogin = () => { window.location.reload(); }; - + const handleLogout = async () => { // Check if we're using proxy auth with a logout URL const proxyAuth = proxyAuthInfo(); @@ -359,26 +377,26 @@ function App() { window.location.href = proxyAuth.logoutURL; return; } - + try { // Import the apiClient to get CSRF token support const { apiFetch, clearAuth } = await import('./utils/apiClient'); - + // Clear any session data - this will include CSRF token const response = await apiFetch('/api/logout', { - method: 'POST' + method: 'POST', }); - + if (!response.ok) { console.error('Logout failed:', response.status); } - + // Clear auth from apiClient clearAuth(); } catch (error) { console.error('Logout error:', error); } - + // Clear all local storage EXCEPT theme preference and logout flag const currentTheme = localStorage.getItem(STORAGE_KEYS.DARK_MODE); localStorage.clear(); @@ -388,12 +406,12 @@ function App() { if (currentTheme) { localStorage.setItem(STORAGE_KEYS.DARK_MODE, currentTheme); } - + // Clear WebSocket connection if (wsStore()) { setWsStore(null); } - + // Force reload to login page window.location.href = '/'; }; @@ -403,243 +421,358 @@ function App() { // Use Show for reactive rendering return ( -
Loading...
} > - } - > + }> - Initializing...}> - - - - -
-
- {/* Header */} -
- -
- - Pulse - - RC - -
-
- -
-
- - - - - - - {connected() ? 'Connected' : reconnecting() ? 'Reconnecting...' : 'Disconnected'} + Initializing...
}> + + + + +
+
+ {/* Header */} +
+ +
+ + + Pulse + + + + RC + + +
+
+ +
+
+ + + + + + + {connected() + ? 'Connected' + : reconnecting() + ? 'Reconnecting...' + : 'Disconnected'} +
+ + + + {proxyAuthInfo()?.username} + + + + +
+
+
+ + {/* Tabs */} +
+
changeTab('main')} + role="tab" + > + + + + + Main +
+
changeTab('storage')} + role="tab" + > + + + + + + Storage +
+
changeTab('backups')} + role="tab" + title="PVE backups, PBS backups, and VM/CT snapshots" + > + + + + + + Backups +
+
changeTab('alerts')} + role="tab" + > + + + + + + Alerts +
+
changeTab('settings')} + role="tab" + > + + + + + Settings + + + +
+
+ + {/* Main Content */} +
+
+ + + + + + + + + + + + + + + + + + + +
+
+ + {/* Footer */} + +
- - - - {proxyAuthInfo()?.username} - - - - -
-
-
- - {/* Tabs */} -
-
changeTab('main')} - role="tab" - > - - - - - Main -
-
changeTab('storage')} - role="tab" - > - - - - - - Storage -
-
changeTab('backups')} - role="tab" - title="PVE backups, PBS backups, and VM/CT snapshots" - > - - - - - - Backups -
-
changeTab('alerts')} - role="tab" - > - - - - - - Alerts -
-
changeTab('settings')} - role="tab" - > - - - - - Settings - - - -
-
- - {/* Main Content */} -
-
- - - - - - - - - - - - - - - - - - - -
-
- - {/* Footer */} - -
-
- - -
-
-
-
+ + + + +
+
); } -export default App;// Test hot-reload comment $(date) +export default App; // Test hot-reload comment $(date) diff --git a/frontend-modern/src/SimpleApp.tsx b/frontend-modern/src/SimpleApp.tsx index c29488c22..4f07372c2 100644 --- a/frontend-modern/src/SimpleApp.tsx +++ b/frontend-modern/src/SimpleApp.tsx @@ -7,45 +7,45 @@ export default function SimpleApp() { onMount(() => { setStatus('Testing API connection...'); - + // Test API fetch('/api/health') - .then(res => { + .then((res) => { setStatus(`API Status: ${res.status}`); return res.json(); }) - .then(d => { + .then((d) => { setData(d); setStatus('API Connected! Testing WebSocket...'); - + // Test WebSocket const ws = new WebSocket(`ws://${window.location.host}/ws`); - + ws.onopen = () => { setWsStatus('WebSocket CONNECTED'); setStatus('Everything working!'); }; - + ws.onerror = (e) => { setWsStatus('WebSocket ERROR'); console.error('WS Error:', e); }; - + ws.onclose = (e) => { setWsStatus(`WebSocket CLOSED: ${e.code} - ${e.reason}`); }; - + ws.onmessage = (e) => { setWsStatus('WebSocket receiving data!'); try { const msg = JSON.parse(e.data); - setData(prev => ({ ...prev, lastMessage: msg.type })); + setData((prev) => ({ ...prev, lastMessage: msg.type })); } catch (err) { console.error('Parse error:', err); } }; }) - .catch(err => { + .catch((err) => { setStatus(`API Error: ${err}`); console.error(err); }); @@ -55,11 +55,15 @@ export default function SimpleApp() {

Pulse System Test


-

Status: {status()}

-

WebSocket: {wsStatus()}

+

+ Status: {status()} +

+

+ WebSocket: {wsStatus()} +


API Data:

{JSON.stringify(data(), null, 2)}
); -} \ No newline at end of file +} diff --git a/frontend-modern/src/Test.tsx b/frontend-modern/src/Test.tsx index 32e522122..93fac58a2 100644 --- a/frontend-modern/src/Test.tsx +++ b/frontend-modern/src/Test.tsx @@ -1,6 +1,8 @@ export default function Test() { - return
-

TEST - APP IS WORKING!

-

If you see this, the basic app infrastructure works.

-
; + return ( +
+

TEST - APP IS WORKING!

+

If you see this, the basic app infrastructure works.

+
+ ); } diff --git a/frontend-modern/src/api/alerts.ts b/frontend-modern/src/api/alerts.ts index 772874728..f043f3793 100644 --- a/frontend-modern/src/api/alerts.ts +++ b/frontend-modern/src/api/alerts.ts @@ -27,7 +27,7 @@ export class AlertsAPI { } }); } - + return apiFetchJSON(`${this.baseUrl}/history?${queryParams}`); } @@ -70,17 +70,22 @@ export class AlertsAPI { }); } - static async bulkAcknowledge(alertIds: string[], user?: string): Promise<{ results: Array<{ alertId: string; success: boolean; error?: string }> }> { + static async bulkAcknowledge( + alertIds: string[], + user?: string, + ): Promise<{ results: Array<{ alertId: string; success: boolean; error?: string }> }> { return apiFetchJSON(`${this.baseUrl}/bulk/acknowledge`, { method: 'POST', body: JSON.stringify({ alertIds, user }), }); } - static async bulkClear(alertIds: string[]): Promise<{ results: Array<{ alertId: string; success: boolean; error?: string }> }> { + static async bulkClear( + alertIds: string[], + ): Promise<{ results: Array<{ alertId: string; success: boolean; error?: string }> }> { return apiFetchJSON(`${this.baseUrl}/bulk/clear`, { method: 'POST', body: JSON.stringify({ alertIds }), }); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/guestMetadata.ts b/frontend-modern/src/api/guestMetadata.ts index 1ebb0a259..3a29ecb4f 100644 --- a/frontend-modern/src/api/guestMetadata.ts +++ b/frontend-modern/src/api/guestMetadata.ts @@ -11,7 +11,6 @@ export interface GuestMetadata { export class GuestMetadataAPI { private static baseUrl = '/api/guests/metadata'; - // Get metadata for a specific guest static async getMetadata(guestId: string): Promise { return apiFetchJSON(`${this.baseUrl}/${encodeURIComponent(guestId)}`); @@ -23,7 +22,10 @@ export class GuestMetadataAPI { } // Update metadata for a guest - static async updateMetadata(guestId: string, metadata: Partial): Promise { + static async updateMetadata( + guestId: string, + metadata: Partial, + ): Promise { return apiFetchJSON(`${this.baseUrl}/${encodeURIComponent(guestId)}`, { method: 'PUT', body: JSON.stringify(metadata), @@ -36,4 +38,4 @@ export class GuestMetadataAPI { method: 'DELETE', }); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/monitoring.ts b/frontend-modern/src/api/monitoring.ts index 7ec449088..fab0247c2 100644 --- a/frontend-modern/src/api/monitoring.ts +++ b/frontend-modern/src/api/monitoring.ts @@ -20,4 +20,4 @@ export class MonitoringAPI { const response = await apiFetch(`${this.baseUrl}/diagnostics/export`); return response.blob(); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/nodes.ts b/frontend-modern/src/api/nodes.ts index c6feda40b..499bf7489 100644 --- a/frontend-modern/src/api/nodes.ts +++ b/frontend-modern/src/api/nodes.ts @@ -17,7 +17,10 @@ export class NodesAPI { }); } - static async updateNode(nodeId: string, node: NodeConfig): Promise<{ success: boolean; message?: string }> { + static async updateNode( + nodeId: string, + node: NodeConfig, + ): Promise<{ success: boolean; message?: string }> { return apiFetchJSON(`${this.baseUrl}/${nodeId}`, { method: 'PUT', body: JSON.stringify(node), @@ -30,9 +33,9 @@ export class NodesAPI { }); } - static async testConnection(node: NodeConfig): Promise<{ + static async testConnection(node: NodeConfig): Promise<{ status: string; - message?: string; + message?: string; isCluster?: boolean; nodeCount?: number; clusterNodeCount?: number; @@ -44,13 +47,13 @@ export class NodesAPI { }); } - static async testExistingNode(nodeId: string): Promise<{ + static async testExistingNode(nodeId: string): Promise<{ status: string; - message?: string; + message?: string; latency?: number; }> { return apiFetchJSON(`${this.baseUrl}/${nodeId}/test`, { method: 'POST', }); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/notifications.ts b/frontend-modern/src/api/notifications.ts index cb1cc3ea3..38cea2ec5 100644 --- a/frontend-modern/src/api/notifications.ts +++ b/frontend-modern/src/api/notifications.ts @@ -58,7 +58,7 @@ export interface Webhook { export interface NotificationTestRequest { type: 'email' | 'webhook'; - config?: Record; // Backend expects different format than frontend types + config?: Record; // Backend expects different format than frontend types webhookId?: string; } @@ -68,7 +68,7 @@ export class NotificationsAPI { // Email configuration static async getEmailConfig(): Promise { const backendConfig = await apiFetchJSON>(`${this.baseUrl}/email`); - + // Backend already returns fields with correct names (server, port) return { enabled: (backendConfig.enabled as boolean) || false, @@ -80,7 +80,7 @@ export class NotificationsAPI { from: (backendConfig.from as string) || '', to: (backendConfig.to as string[]) || [], tls: (backendConfig.tls as boolean) || false, - startTLS: (backendConfig.startTLS as boolean) || false + startTLS: (backendConfig.startTLS as boolean) || false, }; } @@ -96,9 +96,9 @@ export class NotificationsAPI { to: config.to, tls: config.tls || false, startTLS: config.startTLS || false, - provider: config.provider || '' + provider: config.provider || '', }; - + return apiFetchJSON(`${this.baseUrl}/email`, { method: 'PUT', body: JSON.stringify(backendConfig), @@ -141,29 +141,35 @@ export class NotificationsAPI { } // Testing - static async testNotification(request: NotificationTestRequest): Promise<{ success: boolean; message?: string }> { - const body: { method: string; config?: Record; webhookId?: string } = { method: request.type }; - + static async testNotification( + request: NotificationTestRequest, + ): Promise<{ success: boolean; message?: string }> { + const body: { method: string; config?: Record; webhookId?: string } = { + method: request.type, + }; + // Include config if provided for testing without saving if (request.config) { body.config = request.config; } - + // Include webhookId for webhook testing if (request.webhookId) { body.webhookId = request.webhookId; } - + return apiFetchJSON(`${this.baseUrl}/test`, { method: 'POST', body: JSON.stringify(body), }); } - static async testWebhook(webhook: Omit): Promise<{ success: boolean; message?: string }> { + static async testWebhook( + webhook: Omit, + ): Promise<{ success: boolean; message?: string }> { return apiFetchJSON(`${this.baseUrl}/webhooks/test`, { method: 'POST', body: JSON.stringify(webhook), }); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/settings.ts b/frontend-modern/src/api/settings.ts index bdbc822f7..1f4dd12f5 100644 --- a/frontend-modern/src/api/settings.ts +++ b/frontend-modern/src/api/settings.ts @@ -1,7 +1,4 @@ -import type { - SettingsResponse, - SettingsUpdateRequest -} from '@/types/settings'; +import type { SettingsResponse, SettingsUpdateRequest } from '@/types/settings'; import type { SystemConfig } from '@/types/config'; import { apiFetchJSON } from '@/utils/apiClient'; @@ -27,7 +24,7 @@ export class SettingsAPI { body: JSON.stringify(settings), }) as Promise; } - + // System settings update (preferred) - uses SystemConfig type from config.ts static async updateSystemSettings(settings: Partial): Promise { return apiFetchJSON(`${this.baseUrl}/system/settings/update`, { @@ -35,7 +32,7 @@ export class SettingsAPI { body: JSON.stringify(settings), }) as Promise; } - + // Get system settings - returns SystemConfig static async getSystemSettings(): Promise { return apiFetchJSON(`${this.baseUrl}/system/settings`) as Promise; @@ -47,4 +44,4 @@ export class SettingsAPI { body: JSON.stringify(settings), }) as Promise; } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/system.ts b/frontend-modern/src/api/system.ts index 7d51eb7b9..b015128fc 100644 --- a/frontend-modern/src/api/system.ts +++ b/frontend-modern/src/api/system.ts @@ -15,11 +15,11 @@ export class SystemAPI { static async getSystemSettings(): Promise { return apiFetchJSON('/api/system/settings'); } - + static async updateSystemSettings(settings: Partial): Promise { await apiFetchJSON('/api/system/settings/update', { method: 'POST', body: JSON.stringify(settings), }); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/api/updates.ts b/frontend-modern/src/api/updates.ts index de314d131..4f9915a65 100644 --- a/frontend-modern/src/api/updates.ts +++ b/frontend-modern/src/api/updates.ts @@ -49,4 +49,4 @@ export class UpdatesAPI { static async getVersion(): Promise { return apiFetchJSON('/api/version'); } -} \ No newline at end of file +} diff --git a/frontend-modern/src/components/Alerts/CustomRulesTab.tsx b/frontend-modern/src/components/Alerts/CustomRulesTab.tsx index 135223c34..aadd8eb9a 100644 --- a/frontend-modern/src/components/Alerts/CustomRulesTab.tsx +++ b/frontend-modern/src/components/Alerts/CustomRulesTab.tsx @@ -11,31 +11,37 @@ interface CustomRulesTabProps { } export function CustomRulesTab(props: CustomRulesTabProps) { - const deleteRule = (ruleId: string) => { - const updatedRules = props.rules.filter(r => r.id !== ruleId); + const updatedRules = props.rules.filter((r) => r.id !== ruleId); props.onUpdateRules(updatedRules); props.onHasChanges(true); }; const toggleRule = (ruleId: string) => { - const updatedRules = props.rules.map(r => - r.id === ruleId ? { ...r, enabled: !r.enabled } : r + const updatedRules = props.rules.map((r) => + r.id === ruleId ? { ...r, enabled: !r.enabled } : r, ); props.onUpdateRules(updatedRules); props.onHasChanges(true); }; const getFilterDescription = (rule: CustomAlertRule): string => { - return rule.filterConditions.filters.map(filter => { - if (filter.type === 'metric' && filter.field && filter.operator && filter.value !== undefined) { - return `${filter.field} ${filter.operator} ${filter.value}%`; - } else if (filter.type === 'text' && filter.field && filter.value) { - return `${filter.field}: ${filter.value}`; - } else { - return filter.rawText || ''; - } - }).join(` ${rule.filterConditions.logicalOperator} `); + return rule.filterConditions.filters + .map((filter) => { + if ( + filter.type === 'metric' && + filter.field && + filter.operator && + filter.value !== undefined + ) { + return `${filter.field} ${filter.operator} ${filter.value}%`; + } else if (filter.type === 'text' && filter.field && filter.value) { + return `${filter.field}: ${filter.value}`; + } else { + return filter.rawText || ''; + } + }) + .join(` ${rule.filterConditions.logicalOperator} `); }; const getThresholdsSummary = (rule: CustomAlertRule): string => { @@ -62,8 +68,18 @@ export function CustomRulesTab(props: CustomRulesTabProps) { {/* Priority Order Explanation */}
- - + +

Alert Control Methods:

@@ -77,103 +93,149 @@ export function CustomRulesTab(props: CustomRulesTabProps) {
{/* Rules List */} - 0} fallback={ - - - - - )} - title="No custom alert rules" - description="Create rules from the Dashboard by applying filters and choosing Create Alert." - /> - - }> + 0} + fallback={ + + + + + } + title="No custom alert rules" + description="Create rules from the Dashboard by applying filters and choosing Create Alert." + /> + + } + >
b.priority - a.priority)}> {(rule) => (
-
-
-

{rule.name}

- +
+

+ {rule.name} +

+ - {rule.enabled ? 'Active' : 'Disabled'} - - - Priority: {rule.priority} - -
- -

{rule.description}

-
-
-
- - -
-
- -
-
- Filters: -
- - {getFilterDescription(rule)} - -
-
- -
- Thresholds: - - {getThresholdsSummary(rule)} + {rule.enabled ? 'Active' : 'Disabled'} + + + Priority: {rule.priority}
- - -
- Notify: -
- - - Email - - - - - Webhook - - -
-
+ +

+ {rule.description} +

+
+ + +
+
+ +
+
+ + Filters: + +
+ + {getFilterDescription(rule)} + +
+
+ +
+ + Thresholds: + + + {getThresholdsSummary(rule)} + +
+ + +
+ + Notify: + +
+ + + Email + + + + + Webhook + + +
+
+
+
)}
diff --git a/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx b/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx index b32d23501..86ef48d3d 100644 --- a/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx +++ b/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx @@ -1,6 +1,12 @@ import { createSignal, createEffect, Show, For } from 'solid-js'; import { NotificationsAPI } from '@/api/notifications'; -import { formField, labelClass, controlClass, formHelpText, formCheckbox } from '@/components/shared/Form'; +import { + formField, + labelClass, + controlClass, + formHelpText, + formCheckbox, +} from '@/components/shared/Form'; interface EmailProvider { name: string; @@ -152,7 +158,9 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) { props.onChange({ ...props.config, port: parseInt(e.currentTarget.value) || 587 })} + onInput={(e) => + props.onChange({ ...props.config, port: parseInt(e.currentTarget.value) || 587 }) + } placeholder="587" class={controlClass('px-2 py-1.5')} /> @@ -241,7 +249,9 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) { props.onChange({ ...props.config, tls: e.currentTarget.checked })} + onChange={(e) => + props.onChange({ ...props.config, tls: e.currentTarget.checked }) + } class={`${formCheckbox} h-4 w-4`} /> Use TLS @@ -250,7 +260,9 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) { props.onChange({ ...props.config, startTLS: e.currentTarget.checked })} + onChange={(e) => + props.onChange({ ...props.config, startTLS: e.currentTarget.checked }) + } class={`${formCheckbox} h-4 w-4`} /> Use STARTTLS @@ -260,7 +272,9 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) { props.onChange({ ...props.config, rateLimit: parseInt(e.currentTarget.value) })} + onInput={(e) => + props.onChange({ ...props.config, rateLimit: parseInt(e.currentTarget.value) }) + } class={`${controlClass('px-2 py-1 text-sm')} w-20`} /> /min @@ -275,18 +289,24 @@ export function EmailProviderSelect(props: EmailProviderSelectProps) { value={props.config.maxRetries || 3} min={0} max={5} - onInput={(e) => props.onChange({ ...props.config, maxRetries: parseInt(e.currentTarget.value) })} + onInput={(e) => + props.onChange({ ...props.config, maxRetries: parseInt(e.currentTarget.value) }) + } class={controlClass('px-2 py-1 text-sm')} />
- + props.onChange({ ...props.config, retryDelay: parseInt(e.currentTarget.value) })} + onInput={(e) => + props.onChange({ ...props.config, retryDelay: parseInt(e.currentTarget.value) }) + } class={controlClass('px-2 py-1 text-sm')} />
diff --git a/frontend-modern/src/components/Alerts/OverrideModal.tsx b/frontend-modern/src/components/Alerts/OverrideModal.tsx index a42b9a673..dfa8c3079 100644 --- a/frontend-modern/src/components/Alerts/OverrideModal.tsx +++ b/frontend-modern/src/components/Alerts/OverrideModal.tsx @@ -4,13 +4,13 @@ import { ThresholdSlider } from '@/components/Dashboard/ThresholdSlider'; import { SectionHeader } from '@/components/shared/SectionHeader'; interface Override { - id?: string; // Full guest ID (e.g. "Main-node1-105") + id?: string; // Full guest ID (e.g. "Main-node1-105") guestName: string; vmid: number; type: string; node: string; instance?: string; - disabled?: boolean; // Completely disable alerts for this guest + disabled?: boolean; // Completely disable alerts for this guest thresholds: { cpu?: number; memory?: number; @@ -27,14 +27,21 @@ interface OverrideModalProps { onClose: () => void; onSave: (override: Override) => void; existingOverride?: Override; - guests: Array<{ id: string; name: string; vmid: number; type: string; node: string; instance: string }>; + guests: Array<{ + id: string; + name: string; + vmid: number; + type: string; + node: string; + instance: string; + }>; } export function OverrideModal(props: OverrideModalProps) { // Initialize state only when modal opens, not on every render const [selectedGuest, setSelectedGuest] = createSignal(''); const [alertsDisabled, setAlertsDisabled] = createSignal(false); - + // Store the select element ref let selectRef: HTMLSelectElement | undefined; const [thresholds, setThresholds] = createSignal({ @@ -44,9 +51,9 @@ export function OverrideModal(props: OverrideModalProps) { diskRead: 0, diskWrite: 0, networkIn: 0, - networkOut: 0 + networkOut: 0, }); - + const [enabledMetrics, setEnabledMetrics] = createSignal({ cpu: false, memory: false, @@ -54,9 +61,9 @@ export function OverrideModal(props: OverrideModalProps) { diskRead: false, diskWrite: false, networkIn: false, - networkOut: false + networkOut: false, }); - + // Maintain select value when guests change createEffect(() => { if (selectRef && selectedGuest()) { @@ -69,7 +76,7 @@ export function OverrideModal(props: OverrideModalProps) { }); } }); - + // Reset state when modal opens createEffect(() => { if (props.isOpen) { @@ -83,7 +90,7 @@ export function OverrideModal(props: OverrideModalProps) { diskRead: props.existingOverride.thresholds.diskRead || 0, diskWrite: props.existingOverride.thresholds.diskWrite || 0, networkIn: props.existingOverride.thresholds.networkIn || 0, - networkOut: props.existingOverride.thresholds.networkOut || 0 + networkOut: props.existingOverride.thresholds.networkOut || 0, }); setEnabledMetrics({ cpu: props.existingOverride.thresholds.cpu !== undefined, @@ -92,7 +99,7 @@ export function OverrideModal(props: OverrideModalProps) { diskRead: props.existingOverride.thresholds.diskRead !== undefined, diskWrite: props.existingOverride.thresholds.diskWrite !== undefined, networkIn: props.existingOverride.thresholds.networkIn !== undefined, - networkOut: props.existingOverride.thresholds.networkOut !== undefined + networkOut: props.existingOverride.thresholds.networkOut !== undefined, }); } else { // Reset to defaults for new override @@ -105,7 +112,7 @@ export function OverrideModal(props: OverrideModalProps) { diskRead: 0, diskWrite: 0, networkIn: 0, - networkOut: 0 + networkOut: 0, }); setEnabledMetrics({ cpu: false, @@ -114,20 +121,20 @@ export function OverrideModal(props: OverrideModalProps) { diskRead: false, diskWrite: false, networkIn: false, - networkOut: false + networkOut: false, }); } } }); - + const handleSave = () => { - const guest = props.guests.find(g => g.vmid.toString() === selectedGuest()); + const guest = props.guests.find((g) => g.vmid.toString() === selectedGuest()); if (!guest) return; - + const enabledThresholds: Override['thresholds'] = {}; const enabled = enabledMetrics(); const thresh = thresholds(); - + if (enabled.cpu && thresh.cpu !== undefined) enabledThresholds.cpu = thresh.cpu; if (enabled.memory && thresh.memory !== undefined) enabledThresholds.memory = thresh.memory; if (enabled.disk && thresh.disk !== undefined) enabledThresholds.disk = thresh.disk; @@ -135,19 +142,19 @@ export function OverrideModal(props: OverrideModalProps) { if (enabled.diskWrite && thresh.diskWrite) enabledThresholds.diskWrite = thresh.diskWrite; if (enabled.networkIn && thresh.networkIn) enabledThresholds.networkIn = thresh.networkIn; if (enabled.networkOut && thresh.networkOut) enabledThresholds.networkOut = thresh.networkOut; - + props.onSave({ - id: guest.id, // Pass the full guest ID + id: guest.id, // Pass the full guest ID guestName: guest.name, vmid: guest.vmid, type: guest.type, node: guest.node, instance: guest.instance, disabled: alertsDisabled(), - thresholds: enabledThresholds + thresholds: enabledThresholds, }); }; - + return ( @@ -161,7 +168,7 @@ export function OverrideModal(props: OverrideModalProps) { titleClass="text-gray-800 dark:text-gray-200" />
- + {/* Content */}
{/* Guest Selection */} @@ -191,7 +198,7 @@ export function OverrideModal(props: OverrideModalProps) {
- + {/* Disable Alerts Option */}
- + {/* Threshold Overrides */}
- + {/* CPU */}
setEnabledMetrics({...enabledMetrics(), cpu: e.currentTarget.checked})} + onChange={(e) => + setEnabledMetrics({ ...enabledMetrics(), cpu: e.currentTarget.checked }) + } class="mt-1 rounded border-gray-300 dark:border-gray-600" />
@@ -233,7 +242,7 @@ export function OverrideModal(props: OverrideModalProps) {
setThresholds({...thresholds(), cpu: v})} + onChange={(v) => setThresholds({ ...thresholds(), cpu: v })} type="cpu" />
@@ -243,13 +252,15 @@ export function OverrideModal(props: OverrideModalProps) {
- + {/* Memory */}
setEnabledMetrics({...enabledMetrics(), memory: e.currentTarget.checked})} + onChange={(e) => + setEnabledMetrics({ ...enabledMetrics(), memory: e.currentTarget.checked }) + } class="mt-1 rounded border-gray-300 dark:border-gray-600" />
@@ -258,7 +269,7 @@ export function OverrideModal(props: OverrideModalProps) {
setThresholds({...thresholds(), memory: v})} + onChange={(v) => setThresholds({ ...thresholds(), memory: v })} type="memory" />
@@ -268,13 +279,15 @@ export function OverrideModal(props: OverrideModalProps) {
- + {/* Disk */}
setEnabledMetrics({...enabledMetrics(), disk: e.currentTarget.checked})} + onChange={(e) => + setEnabledMetrics({ ...enabledMetrics(), disk: e.currentTarget.checked }) + } class="mt-1 rounded border-gray-300 dark:border-gray-600" />
@@ -283,7 +296,7 @@ export function OverrideModal(props: OverrideModalProps) {
setThresholds({...thresholds(), disk: v})} + onChange={(v) => setThresholds({ ...thresholds(), disk: v })} type="disk" />
@@ -293,21 +306,31 @@ export function OverrideModal(props: OverrideModalProps) {
- + {/* I/O Metrics */}
setEnabledMetrics({...enabledMetrics(), diskRead: e.currentTarget.checked})} + onChange={(e) => + setEnabledMetrics({ + ...enabledMetrics(), + diskRead: e.currentTarget.checked, + }) + } class="mt-1 rounded border-gray-300 dark:border-gray-600" />
- +
setEnabledMetrics({...enabledMetrics(), diskWrite: e.currentTarget.checked})} + onChange={(e) => + setEnabledMetrics({ + ...enabledMetrics(), + diskWrite: e.currentTarget.checked, + }) + } class="mt-1 rounded border-gray-300 dark:border-gray-600" />
{ const val = parseInt(e.currentTarget.value); if (!isNaN(val)) { props.setEditingThresholds({ ...props.editingThresholds(), - [metric]: val + [metric]: val, }); } }} @@ -233,11 +295,12 @@ export function ResourceTable(props: ResourceTableProps) { ); }} - + {/* Alerts column */} - - - - - - - + {/* Actions column */}
- - - - - }> - + + + } + > + - - @@ -349,252 +473,373 @@ export function ResourceTable(props: ResourceTableProps) { - - {(resource) => { - const isEditing = () => props.editingId() === resource.id; - const thresholds = (): Record => { - if (isEditing()) { - return props.editingThresholds(); - } - return resource.thresholds ?? {}; - }; - const displayValue = (metric: string): number => { - const thresh = thresholds(); - const defaults = resource.defaults || {}; - if (isEditing()) { - const val = thresh?.[metric] || defaults[metric]; - return typeof val === 'string' ? (parseFloat(val) || 0) : (val || 0); - } - return resource.thresholds?.[metric] || defaults[metric] || 0; - }; - const isOverridden = (metric: string) => { - return resource.thresholds?.[metric] !== undefined && resource.thresholds?.[metric] !== null; - }; - - return ( - - -
- - {resource.name} - - - ({resource.vmid}) - - - on {resource.node} - - - on {resource.node} - - - - Custom - - -
- - - - {resource.resourceType} - - - - - {resource.status} - - - - {/* Metric columns - dynamically rendered based on resource type */} - - {(column) => { - const metric = column.toLowerCase().replace(' %', '').replace(' mb/s', '').replace('disk r', 'diskRead').replace('disk w', 'diskWrite').replace('net in', 'networkIn').replace('net out', 'networkOut'); - - // Check if this metric applies to this resource type - const showMetric = () => { - if (resource.type === 'node' && ['diskRead', 'diskWrite', 'networkIn', 'networkOut'].includes(metric)) { - return false; - } - if (resource.type === 'pbs') { - // PBS only has CPU and Memory metrics - return ['cpu', 'memory'].includes(metric); - } - if (resource.type === 'storage') { - return metric === 'usage'; - } - return true; - }; - - return ( - - - - }> - - }> - { - const currentThresholds = thresholds(); - const rawValue = currentThresholds[metric]; - return rawValue ?? ''; - })()} - onInput={(e) => { - const parsed = e.currentTarget.value.trim(); - let nextValue: number | undefined; - if (parsed !== '') { - const numeric = Number(parsed); - nextValue = Number.isFinite(numeric) ? numeric : undefined; - } - props.setEditingThresholds({ - ...props.editingThresholds(), - [metric]: nextValue - }); - }} - class="w-14 px-1 py-0.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" - /> - + + {(resource) => { + const isEditing = () => props.editingId() === resource.id; + const thresholds = (): Record => { + if (isEditing()) { + return props.editingThresholds(); + } + return resource.thresholds ?? {}; + }; + const displayValue = (metric: string): number => { + const thresh = thresholds(); + const defaults = resource.defaults || {}; + if (isEditing()) { + const val = thresh?.[metric] || defaults[metric]; + return typeof val === 'string' ? parseFloat(val) || 0 : val || 0; + } + return resource.thresholds?.[metric] || defaults[metric] || 0; + }; + const isOverridden = (metric: string) => { + return ( + resource.thresholds?.[metric] !== undefined && + resource.thresholds?.[metric] !== null + ); + }; + + return ( + + +
+ + {resource.name} + + + ({resource.vmid}) - - ); - }} - - - {/* Alerts column */} - - - - - - - - - - - - - - - - {/* Actions column */} - -
- - - - - }> -
+ + + - - - - - - - -
- - - ); - }} -
- }> + + + + + + + + + + + + {/* Actions column */} + +
+ + + + + } + > + + + + + +
+ + + ); + }} + + } + > - + No {props.title.toLowerCase()} found diff --git a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx index 08fbd6160..45288360a 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTable.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTable.tsx @@ -14,7 +14,7 @@ interface Override { node?: string; instance?: string; disabled?: boolean; - disableConnectivity?: boolean; // For nodes only - disable offline alerts + disableConnectivity?: boolean; // For nodes only - disable offline alerts thresholds: { cpu?: number; memory?: number; @@ -23,7 +23,7 @@ interface Override { diskWrite?: number; networkIn?: number; networkOut?: number; - usage?: number; // For storage devices + usage?: number; // For storage devices }; } @@ -36,7 +36,7 @@ interface SimpleThresholds { diskWrite?: number; networkIn?: number; networkOut?: number; - [key: string]: number | undefined; // Add index signature for compatibility + [key: string]: number | undefined; // Add index signature for compatibility } interface ThresholdsTableProps { @@ -49,9 +49,13 @@ interface ThresholdsTableProps { storage: Storage[]; pbsInstances?: PBSInstance[]; // PBS instances from state guestDefaults: SimpleThresholds; - setGuestDefaults: (value: Record | ((prev: Record) => Record)) => void; + setGuestDefaults: ( + value: Record | ((prev: Record) => Record), + ) => void; nodeDefaults: SimpleThresholds; - setNodeDefaults: (value: Record | ((prev: Record) => Record)) => void; + setNodeDefaults: ( + value: Record | ((prev: Record) => Record), + ) => void; storageDefault: () => number; setStorageDefault: (value: number) => void; timeThreshold: () => number; @@ -65,17 +69,22 @@ interface ThresholdsTableProps { export function ThresholdsTable(props: ThresholdsTableProps) { const [searchTerm, setSearchTerm] = createSignal(''); const [editingId, setEditingId] = createSignal(null); - const [editingThresholds, setEditingThresholds] = createSignal>({}); - + const [editingThresholds, setEditingThresholds] = createSignal< + Record + >({}); + let searchInputRef: HTMLInputElement | undefined; - + // Set up keyboard shortcuts onMount(() => { const handleKeyDown = (e: KeyboardEvent) => { // Skip if user is typing in an input or textarea (unless it's Escape) const target = e.target as HTMLElement; - const isInInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.contentEditable === 'true'; - + const isInInput = + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.contentEditable === 'true'; + // Escape clears search from anywhere if (e.key === 'Escape') { e.preventDefault(); @@ -85,12 +94,12 @@ export function ThresholdsTable(props: ThresholdsTableProps) { } return; } - + // Skip other shortcuts if already in an input if (isInInput) { return; } - + // Any letter/number focuses search and starts typing if (e.key.length === 1 && e.key.match(/[a-z0-9]/i)) { e.preventDefault(); @@ -100,62 +109,70 @@ export function ThresholdsTable(props: ThresholdsTableProps) { } } }; - + document.addEventListener('keydown', handleKeyDown); - + onCleanup(() => { document.removeEventListener('keydown', handleKeyDown); }); }); - + // Helper function to format values with units const formatMetricValue = (metric: string, value: number | undefined): string => { if (value === undefined || value === null) return '0'; - + // Show "Off" for disabled thresholds (0 or negative values) if (value <= 0) return 'Off'; - + // Percentage-based metrics if (metric === 'cpu' || metric === 'memory' || metric === 'disk' || metric === 'usage') { return `${value}%`; } - + // MB/s metrics - if (metric === 'diskRead' || metric === 'diskWrite' || metric === 'networkIn' || metric === 'networkOut') { + if ( + metric === 'diskRead' || + metric === 'diskWrite' || + metric === 'networkIn' || + metric === 'networkOut' + ) { return `${value} MB/s`; } - + return String(value); }; - + // Check if there's an active alert for a resource/metric const hasActiveAlert = (resourceId: string, metric: string): boolean => { if (!props.activeAlerts) return false; const alertKey = `${resourceId}-${metric}`; return alertKey in props.activeAlerts; }; - + // Process nodes with their overrides const nodesWithOverrides = createMemo((prev = []) => { // If we're currently editing, return the previous value to avoid re-renders if (editingId()) { return prev; } - + const search = searchTerm().toLowerCase(); - const overridesMap = new Map(props.overrides().map(o => [o.id, o])); - - const nodes = props.nodes.map(node => { + const overridesMap = new Map(props.overrides().map((o) => [o.id, o])); + + const nodes = props.nodes.map((node) => { const override = overridesMap.get(node.id); - + // Check if any threshold values actually differ from defaults - const hasCustomThresholds = override?.thresholds && - Object.keys(override.thresholds).some(key => { + const hasCustomThresholds = + override?.thresholds && + Object.keys(override.thresholds).some((key) => { const k = key as keyof typeof override.thresholds; - return override.thresholds[k] !== undefined && - override.thresholds[k] !== (props.nodeDefaults as any)[k]; + return ( + override.thresholds[k] !== undefined && + override.thresholds[k] !== (props.nodeDefaults as any)[k] + ); }); - + return { id: node.id, name: node.name, @@ -166,41 +183,44 @@ export function ThresholdsTable(props: ThresholdsTableProps) { disabled: false, disableConnectivity: override?.disableConnectivity || false, thresholds: override?.thresholds || {}, - defaults: props.nodeDefaults + defaults: props.nodeDefaults, }; }); - + if (search) { - return nodes.filter(n => n.name.toLowerCase().includes(search)); + return nodes.filter((n) => n.name.toLowerCase().includes(search)); } return nodes; }, []); - + // Process guests with their overrides and group by node const guestsGroupedByNode = createMemo>((prev = {}) => { // If we're currently editing, return the previous value to avoid re-renders if (editingId()) { return prev; } - + const search = searchTerm().toLowerCase(); - const overridesMap = new Map(props.overrides().map(o => [o.id, o])); - - const guests = props.allGuests().map(guest => { + const overridesMap = new Map(props.overrides().map((o) => [o.id, o])); + + const guests = props.allGuests().map((guest) => { const guestId = guest.id || `${guest.instance}-${guest.node}-${guest.vmid}`; const override = overridesMap.get(guestId); - + // Check if any threshold values actually differ from defaults - const hasCustomThresholds = override?.thresholds && - Object.keys(override.thresholds).some(key => { + const hasCustomThresholds = + override?.thresholds && + Object.keys(override.thresholds).some((key) => { const k = key as keyof typeof override.thresholds; - return override.thresholds[k] !== undefined && - override.thresholds[k] !== (props.guestDefaults as any)[k]; + return ( + override.thresholds[k] !== undefined && + override.thresholds[k] !== (props.guestDefaults as any)[k] + ); }); - + // A guest has an override if it has custom thresholds OR is disabled const hasOverride = hasCustomThresholds || override?.disabled || false; - + return { id: guestId, name: guest.name, @@ -213,120 +233,124 @@ export function ThresholdsTable(props: ThresholdsTableProps) { hasOverride: hasOverride, disabled: override?.disabled || false, thresholds: override?.thresholds || {}, - defaults: props.guestDefaults + defaults: props.guestDefaults, }; }); - - const filteredGuests = search - ? guests.filter(g => - g.name.toLowerCase().includes(search) || - g.vmid?.toString().includes(search) || - g.node?.toLowerCase().includes(search) + + const filteredGuests = search + ? guests.filter( + (g) => + g.name.toLowerCase().includes(search) || + g.vmid?.toString().includes(search) || + g.node?.toLowerCase().includes(search), ) : guests; - + // Group by node const grouped: Record = {}; - filteredGuests.forEach(guest => { + filteredGuests.forEach((guest) => { const node = guest.node || 'Unknown'; if (!grouped[node]) { grouped[node] = []; } grouped[node].push(guest); }); - + // Sort guests within each group by vmid - Object.keys(grouped).forEach(node => { + Object.keys(grouped).forEach((node) => { grouped[node].sort((a, b) => { if (a.vmid && b.vmid) return a.vmid - b.vmid; return a.name.localeCompare(b.name); }); }); - + return grouped; }, {}); - + // Process PBS servers with their overrides const pbsServersWithOverrides = createMemo((prev = []) => { // If we're currently editing, return the previous value to avoid re-renders if (editingId()) { return prev; } - + const search = searchTerm().toLowerCase(); - const overridesMap = new Map(props.overrides().map(o => [o.id, o])); - + const overridesMap = new Map(props.overrides().map((o) => [o.id, o])); + // Get PBS instances from props const pbsInstances = props.pbsInstances || []; - + const pbsServers = pbsInstances .filter((pbs) => (pbs.cpu || 0) > 0 || (pbs.memory || 0) > 0) .map((pbs) => { - // PBS IDs already have "pbs-" prefix from backend, don't double it - const pbsId = pbs.id; - const override = overridesMap.get(pbsId); - - // Check if any threshold values actually differ from defaults - const hasCustomThresholds = override?.thresholds && - Object.keys(override.thresholds).some(key => { - const k = key as keyof typeof override.thresholds; - // PBS uses node defaults for CPU/Memory - return override.thresholds[k] !== undefined && - override.thresholds[k] !== props.nodeDefaults[k as keyof typeof props.nodeDefaults]; - }); - - return { - id: pbsId, - name: pbs.name, - type: 'pbs' as const, - resourceType: 'PBS', - host: pbs.host, - status: pbs.status, - cpu: pbs.cpu, - memory: pbs.memory, - memoryUsed: pbs.memoryUsed, - memoryTotal: pbs.memoryTotal, - uptime: pbs.uptime, - hasOverride: hasCustomThresholds || false, - disabled: false, - disableConnectivity: override?.disableConnectivity || false, - thresholds: override?.thresholds || {}, - defaults: { - cpu: props.nodeDefaults.cpu, - memory: props.nodeDefaults.memory - } - }; - }); - + // PBS IDs already have "pbs-" prefix from backend, don't double it + const pbsId = pbs.id; + const override = overridesMap.get(pbsId); + + // Check if any threshold values actually differ from defaults + const hasCustomThresholds = + override?.thresholds && + Object.keys(override.thresholds).some((key) => { + const k = key as keyof typeof override.thresholds; + // PBS uses node defaults for CPU/Memory + return ( + override.thresholds[k] !== undefined && + override.thresholds[k] !== props.nodeDefaults[k as keyof typeof props.nodeDefaults] + ); + }); + + return { + id: pbsId, + name: pbs.name, + type: 'pbs' as const, + resourceType: 'PBS', + host: pbs.host, + status: pbs.status, + cpu: pbs.cpu, + memory: pbs.memory, + memoryUsed: pbs.memoryUsed, + memoryTotal: pbs.memoryTotal, + uptime: pbs.uptime, + hasOverride: hasCustomThresholds || false, + disabled: false, + disableConnectivity: override?.disableConnectivity || false, + thresholds: override?.thresholds || {}, + defaults: { + cpu: props.nodeDefaults.cpu, + memory: props.nodeDefaults.memory, + }, + }; + }); + if (search) { - return pbsServers.filter((p) => - p.name.toLowerCase().includes(search) || - p.host?.toLowerCase().includes(search) + return pbsServers.filter( + (p) => p.name.toLowerCase().includes(search) || p.host?.toLowerCase().includes(search), ); } return pbsServers; }, []); - + // Process storage with their overrides const storageWithOverrides = createMemo((prev = []) => { // If we're currently editing, return the previous value to avoid re-renders if (editingId()) { return prev; } - + const search = searchTerm().toLowerCase(); - const overridesMap = new Map(props.overrides().map(o => [o.id, o])); - - const storageDevices = props.storage.map(storage => { + const overridesMap = new Map(props.overrides().map((o) => [o.id, o])); + + const storageDevices = props.storage.map((storage) => { const override = overridesMap.get(storage.id); - + // Storage only has usage threshold - const hasCustomThresholds = override?.thresholds?.usage !== undefined && - override.thresholds.usage !== props.storageDefault(); - + const hasCustomThresholds = + override?.thresholds?.usage !== undefined && + override.thresholds.usage !== props.storageDefault(); + // A storage device has an override if it has custom thresholds OR is disabled const hasOverride = hasCustomThresholds || override?.disabled || false; - + return { id: storage.id, name: storage.name, @@ -338,58 +362,61 @@ export function ThresholdsTable(props: ThresholdsTableProps) { hasOverride: hasOverride, disabled: override?.disabled || false, thresholds: override?.thresholds || {}, - defaults: { usage: props.storageDefault() } + defaults: { usage: props.storageDefault() }, }; }); - + if (search) { - return storageDevices.filter(s => - s.name.toLowerCase().includes(search) || - s.node?.toLowerCase().includes(search) + return storageDevices.filter( + (s) => s.name.toLowerCase().includes(search) || s.node?.toLowerCase().includes(search), ); } return storageDevices; }, []); - - + const startEditing = ( resourceId: string, currentThresholds: Record, - defaults: Record + defaults: Record, ) => { setEditingId(resourceId); // Merge defaults with overrides for editing const mergedThresholds = { ...defaults, ...currentThresholds }; setEditingThresholds(mergedThresholds); }; - + const saveEdit = (resourceId: string) => { // Flatten grouped guests to find the resource const allGuests = Object.values(guestsGroupedByNode()).flat(); - const allResources = [...nodesWithOverrides(), ...allGuests, ...storageWithOverrides(), ...pbsServersWithOverrides()]; - const resource = allResources.find(r => r.id === resourceId); + const allResources = [ + ...nodesWithOverrides(), + ...allGuests, + ...storageWithOverrides(), + ...pbsServersWithOverrides(), + ]; + const resource = allResources.find((r) => r.id === resourceId); if (!resource) return; - + const editedThresholds = editingThresholds(); const defaultThresholds = (resource.defaults ?? {}) as Record; - + // Only include values that differ from defaults const overrideThresholds: Record = {}; - Object.keys(editedThresholds).forEach(key => { + Object.keys(editedThresholds).forEach((key) => { const editedValue = editedThresholds[key]; const defaultValue = defaultThresholds[key as keyof typeof defaultThresholds]; if (editedValue !== undefined && editedValue !== defaultValue) { overrideThresholds[key] = editedValue; } }); - + // If no overrides, just cancel the edit if (Object.keys(overrideThresholds).length === 0) { // If there was an existing override, remove it if (resource.hasOverride) { - const newOverrides = props.overrides().filter(o => o.id !== resourceId); + const newOverrides = props.overrides().filter((o) => o.id !== resourceId); props.setOverrides(newOverrides); - + // Also remove from raw config const newRawConfig = { ...props.rawOverridesConfig() }; delete newRawConfig[resourceId]; @@ -399,7 +426,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) { cancelEdit(); return; } - + // Create or update override const override: Override = { id: resourceId, @@ -410,11 +437,11 @@ export function ThresholdsTable(props: ThresholdsTableProps) { node: 'node' in resource ? resource.node : undefined, instance: 'instance' in resource ? resource.instance : undefined, disabled: resource.disabled, - thresholds: overrideThresholds + thresholds: overrideThresholds, }; - + // Update overrides list - const existingIndex = props.overrides().findIndex(o => o.id === resourceId); + const existingIndex = props.overrides().findIndex((o) => o.id === resourceId); if (existingIndex >= 0) { const newOverrides = [...props.overrides()]; newOverrides[existingIndex] = override; @@ -422,15 +449,15 @@ export function ThresholdsTable(props: ThresholdsTableProps) { } else { props.setOverrides([...props.overrides(), override]); } - + // Update raw config const newRawConfig: Record = { ...props.rawOverridesConfig() }; const hysteresisThresholds: RawOverrideConfig = {}; Object.entries(overrideThresholds).forEach(([metric, value]) => { if (value !== undefined && value !== null) { - hysteresisThresholds[metric] = { - trigger: value, - clear: Math.max(0, value - 5) + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, value - 5), }; } }); @@ -439,57 +466,59 @@ export function ThresholdsTable(props: ThresholdsTableProps) { } newRawConfig[resourceId] = hysteresisThresholds; props.setRawOverridesConfig(newRawConfig); - + props.setHasUnsavedChanges(true); setEditingId(null); setEditingThresholds({}); }; - + const cancelEdit = () => { setEditingId(null); setEditingThresholds({}); }; - + const removeOverride = (resourceId: string) => { - props.setOverrides(props.overrides().filter(o => o.id !== resourceId)); - + props.setOverrides(props.overrides().filter((o) => o.id !== resourceId)); + const newRawConfig = { ...props.rawOverridesConfig() }; delete newRawConfig[resourceId]; props.setRawOverridesConfig(newRawConfig); - + props.setHasUnsavedChanges(true); }; - - + const toggleDisabled = (resourceId: string, forceState?: boolean) => { // Flatten grouped guests to find the resource const allGuests = Object.values(guestsGroupedByNode()).flat(); const allResources = [...allGuests, ...storageWithOverrides(), ...pbsServersWithOverrides()]; - const resource = allResources.find(r => r.id === resourceId); - if (!resource || (resource.type !== 'guest' && resource.type !== 'storage' && resource.type !== 'pbs')) return; - + const resource = allResources.find((r) => r.id === resourceId); + if ( + !resource || + (resource.type !== 'guest' && resource.type !== 'storage' && resource.type !== 'pbs') + ) + return; + // Get existing override if it exists - const existingOverride = props.overrides().find(o => o.id === resourceId); - + const existingOverride = props.overrides().find((o) => o.id === resourceId); + // Determine the current disabled state - check the resource's current state, not the override const currentDisabledState = resource.disabled; const newDisabledState = forceState !== undefined ? forceState : !currentDisabledState; - + // Clean the thresholds to exclude 'disabled' if it got in there const cleanThresholds: Record = { ...(existingOverride?.thresholds || {}) }; delete (cleanThresholds as Record).disabled; - + // If enabling (disabled = false) and no custom thresholds exist, remove the override entirely if (!newDisabledState && (!existingOverride || Object.keys(cleanThresholds).length === 0)) { // Remove the override completely - props.setOverrides(props.overrides().filter(o => o.id !== resourceId)); - + props.setOverrides(props.overrides().filter((o) => o.id !== resourceId)); + // Remove from raw config const newRawConfig = { ...props.rawOverridesConfig() }; delete newRawConfig[resourceId]; props.setRawOverridesConfig(newRawConfig); } else { - const override: Override = { id: resourceId, name: resource.name, @@ -499,10 +528,10 @@ export function ThresholdsTable(props: ThresholdsTableProps) { node: 'node' in resource ? resource.node : undefined, instance: 'instance' in resource ? resource.instance : undefined, disabled: newDisabledState, - thresholds: cleanThresholds // Only keep actual threshold overrides + thresholds: cleanThresholds, // Only keep actual threshold overrides }; - - const existingIndex = props.overrides().findIndex(o => o.id === resourceId); + + const existingIndex = props.overrides().findIndex((o) => o.id === resourceId); if (existingIndex >= 0) { const newOverrides = [...props.overrides()]; newOverrides[existingIndex] = override; @@ -510,57 +539,57 @@ export function ThresholdsTable(props: ThresholdsTableProps) { } else { props.setOverrides([...props.overrides(), override]); } - + // Update raw config - const newRawConfig: Record = { ...props.rawOverridesConfig() }; - const hysteresisThresholds: RawOverrideConfig = {}; - + const newRawConfig: Record = { ...props.rawOverridesConfig() }; + const hysteresisThresholds: RawOverrideConfig = {}; + // Only add threshold overrides that differ from defaults Object.entries(override.thresholds).forEach(([metric, value]) => { if (typeof value === 'number') { - hysteresisThresholds[metric] = { - trigger: value, - clear: Math.max(0, value - 5) + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, value - 5), }; } }); - + if (newDisabledState) { hysteresisThresholds.disabled = true; } - + newRawConfig[resourceId] = hysteresisThresholds; props.setRawOverridesConfig(newRawConfig); } - + props.setHasUnsavedChanges(true); }; - - + const toggleNodeConnectivity = (resourceId: string, forceState?: boolean) => { // Find the resource - could be a node or PBS server const nodes = nodesWithOverrides(); const pbsServers = pbsServersWithOverrides(); - const resource = [...nodes, ...pbsServers].find(r => r.id === resourceId); + const resource = [...nodes, ...pbsServers].find((r) => r.id === resourceId); if (!resource || (resource.type !== 'node' && resource.type !== 'pbs')) return; - - // Get existing override if it exists - const existingOverride = props.overrides().find(o => o.id === resourceId); - + + // Get existing override if it exists + const existingOverride = props.overrides().find((o) => o.id === resourceId); + // Determine the current state - use the resource's computed state, not just the override const currentDisableConnectivity = resource.disableConnectivity; - const newDisableConnectivity = forceState !== undefined ? forceState : !currentDisableConnectivity; - + const newDisableConnectivity = + forceState !== undefined ? forceState : !currentDisableConnectivity; + // Clean the thresholds to exclude any unwanted fields const cleanThresholds: Record = { ...(existingOverride?.thresholds || {}) }; delete (cleanThresholds as Record).disabled; delete (cleanThresholds as Record).disableConnectivity; - + // If enabling connectivity alerts (disableConnectivity = false) and no custom thresholds exist, remove the override entirely if (!newDisableConnectivity && Object.keys(cleanThresholds).length === 0) { // Remove the override completely - props.setOverrides(props.overrides().filter(o => o.id !== resourceId)); - + props.setOverrides(props.overrides().filter((o) => o.id !== resourceId)); + // Remove from raw config const newRawConfig = { ...props.rawOverridesConfig() }; delete newRawConfig[resourceId]; @@ -573,11 +602,11 @@ export function ThresholdsTable(props: ThresholdsTableProps) { type: resource.type as 'node' | 'guest' | 'storage', resourceType: resource.resourceType, disableConnectivity: newDisableConnectivity, - thresholds: cleanThresholds + thresholds: cleanThresholds, }; - + // Update overrides list - const existingIndex = props.overrides().findIndex(o => o.id === resourceId); + const existingIndex = props.overrides().findIndex((o) => o.id === resourceId); if (existingIndex >= 0) { const newOverrides = [...props.overrides()]; newOverrides[existingIndex] = override; @@ -585,32 +614,32 @@ export function ThresholdsTable(props: ThresholdsTableProps) { } else { props.setOverrides([...props.overrides(), override]); } - + // Update raw config const newRawConfig = { ...props.rawOverridesConfig() }; const hysteresisThresholds: Record = {}; - + // Add threshold configs Object.entries(cleanThresholds).forEach(([metric, value]) => { if (value !== undefined && value !== null) { - hysteresisThresholds[metric] = { - trigger: value, - clear: Math.max(0, (value as number) - 5) + hysteresisThresholds[metric] = { + trigger: value, + clear: Math.max(0, (value as number) - 5), }; } }); - + if (newDisableConnectivity) { hysteresisThresholds.disableConnectivity = true; } - + newRawConfig[resourceId] = hysteresisThresholds; props.setRawOverridesConfig(newRawConfig); } - + props.setHasUnsavedChanges(true); }; - + return (
{/* Global Settings Section */} @@ -622,338 +651,416 @@ export function ThresholdsTable(props: ThresholdsTableProps) { size="md" />
- +
- {/* Threshold inputs in a responsive layout */} -
-

- Default thresholds for all resources. Individual resources can override these values below. - Enter 0 or -1 to disable specific alerts. -

-
-
-

- VMs & Containers -

-
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, cpu: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + {/* Threshold inputs in a responsive layout */} +
+

+ Default thresholds for all resources. Individual resources can override these values + below. + + Enter 0 or -1 to disable specific alerts. + +

+
+
+

+ VMs & Containers +

+
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + cpu: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, memory: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + memory: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, disk: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + disk: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, diskRead: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + diskRead: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, diskWrite: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + diskWrite: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, networkIn: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + networkIn: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setGuestDefaults((prev) => ({ ...prev, networkOut: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setGuestDefaults((prev) => ({ + ...prev, + networkOut: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
+ />
-
-

- Proxmox Nodes -

-
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setNodeDefaults((prev) => ({ ...prev, cpu: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded +
+
+

+ Proxmox Nodes +

+
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setNodeDefaults((prev) => ({ + ...prev, + cpu: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setNodeDefaults((prev) => ({ ...prev, memory: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setNodeDefaults((prev) => ({ + ...prev, + memory: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setNodeDefaults((prev) => ({ ...prev, disk: Number.isNaN(value) ? 0 : value })); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded + /> +
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setNodeDefaults((prev) => ({ + ...prev, + disk: Number.isNaN(value) ? 0 : value, + })); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
+ />
-
-

- Storage -

-
-
- - { - const value = parseInt(e.currentTarget.value, 10); - props.setStorageDefault(Number.isNaN(value) ? 0 : value); - props.setHasUnsavedChanges(true); - }} - class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded +
+
+

+ Storage +

+
+
+ + { + const value = parseInt(e.currentTarget.value, 10); + props.setStorageDefault(Number.isNaN(value) ? 0 : value); + props.setHasUnsavedChanges(true); + }} + class="w-full px-3 py-1.5 text-sm text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> -
+ />
- {/* Alert delay settings per resource type */} -
-
-

Alert Delay (seconds before triggering)

-
-
- - { - props.setTimeThresholds({...props.timeThresholds(), guest: parseInt(e.currentTarget.value) || 0}); - props.setHasUnsavedChanges(true); - }} - class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded +
+ {/* Alert delay settings per resource type */} +
+
+

+ Alert Delay (seconds before triggering) +

+
+
+ + { + props.setTimeThresholds({ + ...props.timeThresholds(), + guest: parseInt(e.currentTarget.value) || 0, + }); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" - /> -
-
- - { - props.setTimeThresholds({...props.timeThresholds(), node: parseInt(e.currentTarget.value) || 0}); - props.setHasUnsavedChanges(true); - }} - class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" - /> -
-
- - { - props.setTimeThresholds({...props.timeThresholds(), storage: parseInt(e.currentTarget.value) || 0}); - props.setHasUnsavedChanges(true); - }} - class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" - /> -
-
- - { - props.setTimeThresholds({...props.timeThresholds(), pbs: parseInt(e.currentTarget.value) || 0}); - props.setHasUnsavedChanges(true); - }} - class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded - bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" - /> -
+ />
+
+ + { + props.setTimeThresholds({ + ...props.timeThresholds(), + node: parseInt(e.currentTarget.value) || 0, + }); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
+ + { + props.setTimeThresholds({ + ...props.timeThresholds(), + storage: parseInt(e.currentTarget.value) || 0, + }); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
+ + { + props.setTimeThresholds({ + ...props.timeThresholds(), + pbs: parseInt(e.currentTarget.value) || 0, + }); + props.setHasUnsavedChanges(true); + }} + class="w-14 px-1 py-0.5 text-xs text-center border border-gray-300 dark:border-gray-600 rounded + bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100" + /> +
+
- + {/* Reset button row */}
- @@ -986,7 +1098,7 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
- + {/* Search Bar */}
- - + + -
- + {/* Nodes Table */} 0}> - + {/* PBS Servers Table */} 0}> - + {/* Guests Table */} 0}> - + {/* Storage Table */} 0}> (null); - const [formData, setFormData] = createSignal & { service: string; payloadTemplate?: string }>({ + const [formData, setFormData] = createSignal< + Omit & { service: string; payloadTemplate?: string } + >({ name: '', url: '', method: 'POST', service: 'generic', headers: { 'Content-Type': 'application/json' }, enabled: true, - payloadTemplate: '' + payloadTemplate: '', }); const [templates, setTemplates] = createSignal([]); const [showServiceDropdown, setShowServiceDropdown] = createSignal(false); - + // Track header inputs separately to avoid focus loss - const [headerInputs, setHeaderInputs] = createSignal>([]); - + const [headerInputs, setHeaderInputs] = createSignal< + Array<{ id: string; key: string; value: string }> + >([]); + // Load webhook templates createEffect(async () => { try { @@ -48,26 +58,26 @@ export function WebhookConfig(props: WebhookConfigProps) { console.error('Failed to load webhook templates:', err); } }); - + const saveWebhook = () => { const data = formData(); if (!data.name || !data.url) return; - + // Build headers from headerInputs const headers: Record = {}; - headerInputs().forEach(input => { + headerInputs().forEach((input) => { if (input.key) { headers[input.key] = input.value; } }); - + if (editingId()) { - props.onUpdate({ - ...data, - id: editingId()!, + props.onUpdate({ + ...data, + id: editingId()!, headers, service: data.service, - template: data.payloadTemplate + template: data.payloadTemplate, }); setEditingId(null); setAdding(false); @@ -81,7 +91,7 @@ export function WebhookConfig(props: WebhookConfigProps) { headers, enabled: data.enabled, service: data.service, - template: data.payloadTemplate + template: data.payloadTemplate, }; props.onAdd(newWebhook); // Reset form and close the adding panel @@ -92,13 +102,13 @@ export function WebhookConfig(props: WebhookConfigProps) { service: 'generic', headers: { 'Content-Type': 'application/json' }, enabled: true, - payloadTemplate: '' + payloadTemplate: '', }); setHeaderInputs([]); setAdding(false); } }; - + const cancelForm = () => { setAdding(false); setEditingId(null); @@ -109,11 +119,11 @@ export function WebhookConfig(props: WebhookConfigProps) { service: 'generic', headers: { 'Content-Type': 'application/json' }, enabled: true, - payloadTemplate: '' + payloadTemplate: '', }); setHeaderInputs([]); }; - + const editWebhook = (webhook: Webhook) => { if (webhook.id) { setEditingId(webhook.id); @@ -121,7 +131,7 @@ export function WebhookConfig(props: WebhookConfigProps) { setFormData({ ...webhook, service: webhook.service || 'generic', - payloadTemplate: webhook.template || '' + payloadTemplate: webhook.template || '', }); // Set up header inputs for editing const headers = webhook.headers || {}; @@ -129,14 +139,14 @@ export function WebhookConfig(props: WebhookConfigProps) { Object.entries(headers).map(([key, value], index) => ({ id: `header-${Date.now()}-${index}`, key, - value - })) + value, + })), ); setAdding(true); }; - + const selectService = (service: string) => { - const template = templates().find(t => t.service === service); + const template = templates().find((t) => t.service === service); if (template) { setFormData({ ...formData(), @@ -146,7 +156,7 @@ export function WebhookConfig(props: WebhookConfigProps) { name: formData().name || template.name, // Clear the payload template when switching services // Only generic service should have custom payloads - payloadTemplate: service === 'generic' ? formData().payloadTemplate : '' + payloadTemplate: service === 'generic' ? formData().payloadTemplate : '', }); // Update header inputs when switching services const headers = template.headers || {}; @@ -154,14 +164,14 @@ export function WebhookConfig(props: WebhookConfigProps) { Object.entries(headers).map(([key, value], index) => ({ id: `header-${Date.now()}-${index}`, key, - value - })) + value, + })), ); } setShowServiceDropdown(false); }; - - const currentTemplate = () => templates().find(t => t.service === formData().service); + + const currentTemplate = () => templates().find((t) => t.service === formData().service); const serviceName = (service: string) => { const names: Record = { generic: 'Generic', @@ -173,19 +183,19 @@ export function WebhookConfig(props: WebhookConfigProps) { pagerduty: 'PagerDuty', pushover: 'Pushover', gotify: 'Gotify', - ntfy: 'ntfy' + ntfy: 'ntfy', }; return names[service] || service; }; - + const toggleAllWebhooks = (enabled: boolean) => { - props.webhooks.forEach(webhook => { + props.webhooks.forEach((webhook) => { props.onUpdate({ ...webhook, enabled }); }); }; - const allEnabled = () => props.webhooks.every(w => w.enabled); - const someEnabled = () => props.webhooks.some(w => w.enabled); + const allEnabled = () => props.webhooks.every((w) => w.enabled); + const someEnabled = () => props.webhooks.some((w) => w.enabled); return (
@@ -195,19 +205,22 @@ export function WebhookConfig(props: WebhookConfigProps) { {/* Quick Actions Bar */}
- {props.webhooks.filter(w => w.enabled).length} of {props.webhooks.length} webhooks enabled + {props.webhooks.filter((w) => w.enabled).length} of {props.webhooks.length} webhooks + enabled
@@ -216,9 +229,7 @@ export function WebhookConfig(props: WebhookConfigProps) { {(webhook) => (
- - {webhook.name} - + {webhook.name}
- + {/* Add/Edit Form */}
@@ -280,19 +291,34 @@ export function WebhookConfig(props: WebhookConfigProps) { -
- +
- + {(service) => ( -
- {service === 'generic' ? 'Custom webhook endpoint' : - service === 'discord' ? 'Discord server webhook' : - service === 'slack' ? 'Slack incoming webhook' : - service === 'telegram' ? 'Telegram bot notifications' : - service === 'teams' ? 'Microsoft Teams webhook' : - service === 'teams-adaptive' ? 'Teams with Adaptive Cards' : - service === 'pushover' ? 'Mobile push notifications' : - service === 'gotify' ? 'Self-hosted push notifications' : - service === 'ntfy' ? 'Push notifications via ntfy.sh' : - 'PagerDuty Events API v2'} + {service === 'generic' + ? 'Custom webhook endpoint' + : service === 'discord' + ? 'Discord server webhook' + : service === 'slack' + ? 'Slack incoming webhook' + : service === 'telegram' + ? 'Telegram bot notifications' + : service === 'teams' + ? 'Microsoft Teams webhook' + : service === 'teams-adaptive' + ? 'Teams with Adaptive Cards' + : service === 'pushover' + ? 'Mobile push notifications' + : service === 'gotify' + ? 'Self-hosted push notifications' + : service === 'ntfy' + ? 'Push notifications via ntfy.sh' + : 'PagerDuty Events API v2'}
)}
- +

@@ -330,13 +365,11 @@ export function WebhookConfig(props: WebhookConfigProps) {

- + {/* Basic Configuration */}
- +
- +
- +
- +
- +
- + {/* Custom Payload Template - only show for generic service */}
@@ -386,7 +415,9 @@ export function WebhookConfig(props: WebhookConfigProps) {