From b1b74b2bd82224ddcda4910007d58fb85d1345a2 Mon Sep 17 00:00:00 2001 From: Pulse Monitor Date: Tue, 12 Aug 2025 20:56:43 +0000 Subject: [PATCH] fix: permanently solve frontend embed sync issue - Add build.sh script that ensures frontend is copied before Go build - Update backend-watch.sh to sync frontend on every restart - Remove duplicate frontend files from internal/api/ - Frontend embed directory already in .gitignore - No more manual copying needed - just run ./build.sh --- build.sh | 26 + .../src/components/Settings/Settings.tsx | 2 +- internal/api/frontend-modern/.gitignore | 32 - internal/api/frontend-modern/index.html | 16 - .../api/frontend-modern/postcss.config.js | 6 - internal/api/frontend-modern/public/logo.svg | 16 - internal/api/frontend-modern/src/App.tsx | 295 -- .../api/frontend-modern/src/api/alerts.ts | 107 - .../api/frontend-modern/src/api/monitoring.ts | 37 - internal/api/frontend-modern/src/api/nodes.ts | 103 - .../frontend-modern/src/api/notifications.ts | 231 -- .../api/frontend-modern/src/api/settings.ts | 91 - .../api/frontend-modern/src/api/updates.ts | 69 - .../src/components/Alerts/CustomRulesTab.tsx | 178 -- .../components/Alerts/EmailProviderSelect.tsx | 332 -- .../src/components/Alerts/OverrideModal.tsx | 340 -- .../src/components/Alerts/WebhookConfig.tsx | 362 --- .../src/components/Backups/Backups.tsx | 46 - .../src/components/Backups/UnifiedBackups.tsx | 1567 ---------- .../src/components/Dashboard/Dashboard.tsx | 729 ----- .../src/components/Dashboard/GuestRow.tsx | 176 -- .../src/components/Dashboard/IOMetric.tsx | 30 - .../src/components/Dashboard/MetricBar.tsx | 68 - .../src/components/Dashboard/NodeCard.tsx | 164 - .../components/Dashboard/ThresholdSlider.tsx | 123 - .../src/components/ErrorBoundary.tsx | 132 - .../src/components/NotificationContainer.tsx | 25 - .../src/components/NotificationToast.tsx | 110 - .../Settings/BatchCredentialModal.tsx | 319 -- .../components/Settings/DiscoveryModal.tsx | 371 --- .../src/components/Settings/NodeModal.tsx | 1092 ------- .../Settings/RegistrationTokens.tsx | 411 --- .../src/components/Settings/Settings.tsx | 2070 ------------ .../src/components/Storage/Storage.tsx | 460 --- .../src/components/Toast/Toast.tsx | 146 - .../src/components/shared/AlertIndicators.tsx | 115 - .../src/components/shared/ScrollableTable.tsx | 58 - .../src/components/shared/Tooltip.tsx | 121 - internal/api/frontend-modern/src/constants.ts | 69 - .../src/hooks/useIntersectionObserver.ts | 45 - internal/api/frontend-modern/src/index.css | 325 -- internal/api/frontend-modern/src/index.tsx | 19 - .../api/frontend-modern/src/pages/Alerts.tsx | 2767 ----------------- .../api/frontend-modern/src/stores/events.ts | 32 - .../src/stores/notifications.ts | 44 - .../src/stores/websocket-global.ts | 20 - .../frontend-modern/src/stores/websocket.ts | 318 -- .../api/frontend-modern/src/types/alerts.ts | 94 - internal/api/frontend-modern/src/types/api.ts | 361 --- .../api/frontend-modern/src/types/backups.ts | 94 - .../frontend-modern/src/types/monitoring.ts | 141 - .../api/frontend-modern/src/types/nodes.ts | 69 - .../api/frontend-modern/src/types/settings.ts | 67 - .../api/frontend-modern/src/utils/alerts.ts | 60 - .../frontend-modern/src/utils/clipboard.ts | 24 - .../frontend-modern/src/utils/errorHandler.ts | 55 - .../api/frontend-modern/src/utils/format.ts | 75 - .../frontend-modern/src/utils/localStorage.ts | 98 - .../api/frontend-modern/src/utils/logger.ts | 22 - .../frontend-modern/src/utils/searchQuery.ts | 322 -- .../api/frontend-modern/src/utils/toast.ts | 18 - .../api/frontend-modern/src/vite-env.d.ts | 1 - .../api/frontend-modern/tailwind.config.js | 20 - internal/api/frontend-modern/tsconfig.json | 30 - .../api/frontend-modern/tsconfig.node.json | 11 - internal/api/frontend-modern/vite.config.ts | 35 - 66 files changed, 27 insertions(+), 15785 deletions(-) create mode 100755 build.sh delete mode 100644 internal/api/frontend-modern/.gitignore delete mode 100644 internal/api/frontend-modern/index.html delete mode 100644 internal/api/frontend-modern/postcss.config.js delete mode 100644 internal/api/frontend-modern/public/logo.svg delete mode 100644 internal/api/frontend-modern/src/App.tsx delete mode 100644 internal/api/frontend-modern/src/api/alerts.ts delete mode 100644 internal/api/frontend-modern/src/api/monitoring.ts delete mode 100644 internal/api/frontend-modern/src/api/nodes.ts delete mode 100644 internal/api/frontend-modern/src/api/notifications.ts delete mode 100644 internal/api/frontend-modern/src/api/settings.ts delete mode 100644 internal/api/frontend-modern/src/api/updates.ts delete mode 100644 internal/api/frontend-modern/src/components/Alerts/CustomRulesTab.tsx delete mode 100644 internal/api/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx delete mode 100644 internal/api/frontend-modern/src/components/Alerts/OverrideModal.tsx delete mode 100644 internal/api/frontend-modern/src/components/Alerts/WebhookConfig.tsx delete mode 100644 internal/api/frontend-modern/src/components/Backups/Backups.tsx delete mode 100644 internal/api/frontend-modern/src/components/Backups/UnifiedBackups.tsx delete mode 100644 internal/api/frontend-modern/src/components/Dashboard/Dashboard.tsx delete mode 100644 internal/api/frontend-modern/src/components/Dashboard/GuestRow.tsx delete mode 100644 internal/api/frontend-modern/src/components/Dashboard/IOMetric.tsx delete mode 100644 internal/api/frontend-modern/src/components/Dashboard/MetricBar.tsx delete mode 100644 internal/api/frontend-modern/src/components/Dashboard/NodeCard.tsx delete mode 100644 internal/api/frontend-modern/src/components/Dashboard/ThresholdSlider.tsx delete mode 100644 internal/api/frontend-modern/src/components/ErrorBoundary.tsx delete mode 100644 internal/api/frontend-modern/src/components/NotificationContainer.tsx delete mode 100644 internal/api/frontend-modern/src/components/NotificationToast.tsx delete mode 100644 internal/api/frontend-modern/src/components/Settings/BatchCredentialModal.tsx delete mode 100644 internal/api/frontend-modern/src/components/Settings/DiscoveryModal.tsx delete mode 100644 internal/api/frontend-modern/src/components/Settings/NodeModal.tsx delete mode 100644 internal/api/frontend-modern/src/components/Settings/RegistrationTokens.tsx delete mode 100644 internal/api/frontend-modern/src/components/Settings/Settings.tsx delete mode 100644 internal/api/frontend-modern/src/components/Storage/Storage.tsx delete mode 100644 internal/api/frontend-modern/src/components/Toast/Toast.tsx delete mode 100644 internal/api/frontend-modern/src/components/shared/AlertIndicators.tsx delete mode 100644 internal/api/frontend-modern/src/components/shared/ScrollableTable.tsx delete mode 100644 internal/api/frontend-modern/src/components/shared/Tooltip.tsx delete mode 100644 internal/api/frontend-modern/src/constants.ts delete mode 100644 internal/api/frontend-modern/src/hooks/useIntersectionObserver.ts delete mode 100644 internal/api/frontend-modern/src/index.css delete mode 100644 internal/api/frontend-modern/src/index.tsx delete mode 100644 internal/api/frontend-modern/src/pages/Alerts.tsx delete mode 100644 internal/api/frontend-modern/src/stores/events.ts delete mode 100644 internal/api/frontend-modern/src/stores/notifications.ts delete mode 100644 internal/api/frontend-modern/src/stores/websocket-global.ts delete mode 100644 internal/api/frontend-modern/src/stores/websocket.ts delete mode 100644 internal/api/frontend-modern/src/types/alerts.ts delete mode 100644 internal/api/frontend-modern/src/types/api.ts delete mode 100644 internal/api/frontend-modern/src/types/backups.ts delete mode 100644 internal/api/frontend-modern/src/types/monitoring.ts delete mode 100644 internal/api/frontend-modern/src/types/nodes.ts delete mode 100644 internal/api/frontend-modern/src/types/settings.ts delete mode 100644 internal/api/frontend-modern/src/utils/alerts.ts delete mode 100644 internal/api/frontend-modern/src/utils/clipboard.ts delete mode 100644 internal/api/frontend-modern/src/utils/errorHandler.ts delete mode 100644 internal/api/frontend-modern/src/utils/format.ts delete mode 100644 internal/api/frontend-modern/src/utils/localStorage.ts delete mode 100644 internal/api/frontend-modern/src/utils/logger.ts delete mode 100644 internal/api/frontend-modern/src/utils/searchQuery.ts delete mode 100644 internal/api/frontend-modern/src/utils/toast.ts delete mode 100644 internal/api/frontend-modern/src/vite-env.d.ts delete mode 100644 internal/api/frontend-modern/tailwind.config.js delete mode 100644 internal/api/frontend-modern/tsconfig.json delete mode 100644 internal/api/frontend-modern/tsconfig.node.json delete mode 100644 internal/api/frontend-modern/vite.config.ts diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..ce43c8ce7 --- /dev/null +++ b/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Build script for Pulse that ensures frontend is in the right place for embedding + +set -e + +echo "Building Pulse..." + +# Build frontend +echo "Building frontend..." +cd frontend-modern +npm run build + +# Ensure the embed directory exists and is up to date +echo "Preparing frontend for embedding..." +mkdir -p ../internal/api/frontend-modern +rm -rf ../internal/api/frontend-modern/dist +cp -r dist ../internal/api/frontend-modern/ + +# Build backend with embedded frontend +echo "Building backend..." +cd .. +go build -o pulse ./cmd/pulse + +echo "Build complete!" +echo "To run: ./pulse or sudo systemctl restart pulse-backend" \ No newline at end of file diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index b1e9bd3ac..e266a9b89 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -1476,7 +1476,7 @@ const Settings: Component = () => { {/* Security Tab */}
- +

Quick Security Setup

diff --git a/internal/api/frontend-modern/.gitignore b/internal/api/frontend-modern/.gitignore deleted file mode 100644 index 7cc38cf5c..000000000 --- a/internal/api/frontend-modern/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# Dependencies -node_modules -.pnp -.pnp.js - -# Testing -coverage - -# Production -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* \ No newline at end of file diff --git a/internal/api/frontend-modern/index.html b/internal/api/frontend-modern/index.html deleted file mode 100644 index 78bd7f095..000000000 --- a/internal/api/frontend-modern/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - Pulse - - - -
- - - - \ No newline at end of file diff --git a/internal/api/frontend-modern/postcss.config.js b/internal/api/frontend-modern/postcss.config.js deleted file mode 100644 index e99ebc2c0..000000000 --- a/internal/api/frontend-modern/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} \ No newline at end of file diff --git a/internal/api/frontend-modern/public/logo.svg b/internal/api/frontend-modern/public/logo.svg deleted file mode 100644 index 22b2ec661..000000000 --- a/internal/api/frontend-modern/public/logo.svg +++ /dev/null @@ -1,16 +0,0 @@ - - Pulse Logo - - - - - \ No newline at end of file diff --git a/internal/api/frontend-modern/src/App.tsx b/internal/api/frontend-modern/src/App.tsx deleted file mode 100644 index efb7002bd..000000000 --- a/internal/api/frontend-modern/src/App.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { Show, createSignal, createContext, useContext, createEffect, onMount } from 'solid-js'; -import { getGlobalWebSocketStore } from './stores/websocket-global'; -import { Dashboard } from './components/Dashboard/Dashboard'; -import StorageComponent from './components/Storage/Storage'; -import Backups from './components/Backups/Backups'; -import Settings from './components/Settings/Settings'; -import { Alerts } from './pages/Alerts'; -import { ToastContainer } from './components/Toast/Toast'; -import { ErrorBoundary } from './components/ErrorBoundary'; -import NotificationContainer from './components/NotificationContainer'; -import { logger } from './utils/logger'; -import { POLLING_INTERVALS, STORAGE_KEYS } from './constants'; -import { UpdatesAPI } from './api/updates'; -import type { VersionInfo } from './api/updates'; - -type TabType = 'main' | 'storage' | 'backups' | 'alerts' | 'settings'; - -// Enhanced store type with proper typing -type EnhancedStore = ReturnType; - -// Export WebSocket context for other components -export const WebSocketContext = createContext(); -export const useWebSocket = () => { - const context = useContext(WebSocketContext); - if (!context) { - throw new Error('useWebSocket must be used within WebSocketContext.Provider'); - } - return context; -}; - -function App() { - // Get singleton WebSocket store - const wsStore = getGlobalWebSocketStore(); - const { state, connected, reconnecting } = wsStore; - - // Data update indicator - const [dataUpdated, setDataUpdated] = createSignal(false); - let updateTimeout: number; - - // Flash indicator when data updates - createEffect(() => { - // Watch for state changes - const updateTime = state.lastUpdate; - if (updateTime && updateTime !== '') { - setDataUpdated(true); - window.clearTimeout(updateTimeout); - updateTimeout = window.setTimeout(() => setDataUpdated(false), POLLING_INTERVALS.DATA_FLASH); - } - }); - - // Tab management - const [activeTab, setActiveTab] = createSignal('main'); - - // Version info - const [versionInfo, setVersionInfo] = createSignal(null); - - // Dark mode - const [darkMode, setDarkMode] = createSignal( - localStorage.getItem(STORAGE_KEYS.DARK_MODE) === 'true' || - window.matchMedia('(prefers-color-scheme: dark)').matches - ); - - // Toggle dark mode - const toggleDarkMode = () => { - const newMode = !darkMode(); - setDarkMode(newMode); - localStorage.setItem(STORAGE_KEYS.DARK_MODE, String(newMode)); - if (newMode) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); - } - logger.info('Theme changed', { mode: newMode ? 'dark' : 'light' }); - }; - - // Initialize dark mode - if (darkMode()) { - document.documentElement.classList.add('dark'); - } - - // Load version info on mount - onMount(async () => { - try { - const version = await UpdatesAPI.getVersion(); - setVersionInfo(version); - } catch (error) { - console.error('Failed to load version:', error); - } - }); - - // Pass through the store directly - const enhancedStore: EnhancedStore = wsStore; - - return ( - - -
-
- {/* Header */} -
- -
- - Pulse - - RC - -
-
- -
-
- - - - - - - {connected() ? 'Connected' : reconnecting() ? 'Reconnecting...' : 'Disconnected'} -
-
-
-
- - {/* Tabs */} -
-
setActiveTab('main')} - role="tab" - > - - - - - Main -
-
setActiveTab('storage')} - role="tab" - > - - - - - - Storage -
-
setActiveTab('backups')} - role="tab" - title="PVE backups, PBS backups, and VM/CT snapshots" - > - - - - - - Backups -
-
setActiveTab('alerts')} - role="tab" - > - - - - - - Alerts -
-
setActiveTab('settings')} - role="tab" - > - - - - - Settings -
-
- - {/* Main Content */} -
-
- - - - - - - - - - - - - - - - - - - -
-
- - {/* Footer */} - -
-
- - -
-
- ); -} - -export default App; \ No newline at end of file diff --git a/internal/api/frontend-modern/src/api/alerts.ts b/internal/api/frontend-modern/src/api/alerts.ts deleted file mode 100644 index d9b2426f8..000000000 --- a/internal/api/frontend-modern/src/api/alerts.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type { Alert } from '@/types/api'; -import type { AlertConfig } from '@/types/alerts'; -// Error handling utilities available for future use -// import { handleError, createErrorBoundary } from '@/utils/errorHandler'; - -export class AlertsAPI { - private static baseUrl = '/api/alerts'; - - static async getActive(): Promise { - const response = await fetch(`${this.baseUrl}/active`); - if (!response.ok) { - throw new Error('Failed to fetch active alerts'); - } - return response.json(); - } - - static async getHistory(params?: { - limit?: number; - offset?: number; - startTime?: string; - endTime?: string; - severity?: 'warning' | 'critical' | 'all'; - resourceId?: string; - }): Promise { - const queryParams = new URLSearchParams(); - if (params) { - Object.entries(params).forEach(([key, value]) => { - if (value !== undefined) { - queryParams.append(key, value.toString()); - } - }); - } - - const response = await fetch(`${this.baseUrl}/history?${queryParams}`); - if (!response.ok) { - throw new Error('Failed to fetch alert history'); - } - return response.json(); - } - - // Removed unused config methods - not implemented in backend - - static async acknowledge(alertId: string, user?: string): Promise<{ success: boolean }> { - const response = await fetch(`${this.baseUrl}/${alertId}/acknowledge`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ user }), - }); - - if (!response.ok) { - throw new Error('Failed to acknowledge alert'); - } - - return response.json(); - } - - // Alert configuration methods - static async getConfig(): Promise { - const response = await fetch(`${this.baseUrl}/config`); - if (!response.ok) { - throw new Error('Failed to fetch alert configuration'); - } - return response.json(); - } - - static async updateConfig(config: AlertConfig): Promise<{ success: boolean }> { - const response = await fetch(`${this.baseUrl}/config`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(config), - }); - - if (!response.ok) { - throw new Error('Failed to update alert configuration'); - } - - return response.json(); - } - - static async clearAlert(alertId: string): Promise<{ success: boolean }> { - const response = await fetch(`${this.baseUrl}/${alertId}/clear`, { - method: 'POST', - }); - - if (!response.ok) { - throw new Error('Failed to clear alert'); - } - - return response.json(); - } - - static async clearHistory(): Promise<{ success: boolean }> { - const response = await fetch(`${this.baseUrl}/history`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error('Failed to clear alert history'); - } - - return response.json(); - } -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/api/monitoring.ts b/internal/api/frontend-modern/src/api/monitoring.ts deleted file mode 100644 index f46c6d8cb..000000000 --- a/internal/api/frontend-modern/src/api/monitoring.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { State, Performance, Stats } from '@/types/api'; - -export class MonitoringAPI { - private static baseUrl = '/api'; - - static async getState(): Promise { - const response = await fetch(`${this.baseUrl}/state`); - if (!response.ok) { - throw new Error('Failed to fetch monitoring state'); - } - return response.json(); - } - - static async getPerformance(): Promise { - const response = await fetch(`${this.baseUrl}/performance`); - if (!response.ok) { - throw new Error('Failed to fetch performance metrics'); - } - return response.json(); - } - - static async getStats(): Promise { - const response = await fetch(`${this.baseUrl}/stats`); - if (!response.ok) { - throw new Error('Failed to fetch system stats'); - } - return response.json(); - } - - static async exportDiagnostics(): Promise { - const response = await fetch(`${this.baseUrl}/diagnostics/export`); - if (!response.ok) { - throw new Error('Failed to export diagnostics'); - } - return response.blob(); - } -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/api/nodes.ts b/internal/api/frontend-modern/src/api/nodes.ts deleted file mode 100644 index 0b679187d..000000000 --- a/internal/api/frontend-modern/src/api/nodes.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { NodeConfig } from '../types/nodes'; - -export class NodesAPI { - private static readonly baseUrl = '/api/config/nodes'; - - static async getNodes(): Promise { - const response = await fetch(this.baseUrl); - if (!response.ok) { - throw new Error('Failed to fetch nodes'); - } - // The API returns an array of nodes directly - const nodes: NodeConfig[] = await response.json(); - return nodes; - } - - static async addNode(node: NodeConfig): Promise<{ success: boolean; message?: string }> { - const response = await fetch(this.baseUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(node), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to add node: ${error}`); - } - - return response.json(); - } - - static async updateNode(nodeId: string, node: NodeConfig): Promise<{ success: boolean; message?: string }> { - const response = await fetch(`${this.baseUrl}/${nodeId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(node), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to update node: ${error}`); - } - - return response.json(); - } - - static async deleteNode(nodeId: string): Promise<{ success: boolean; message?: string }> { - const response = await fetch(`${this.baseUrl}/${nodeId}`, { - method: 'DELETE', - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to delete node: ${error}`); - } - - return response.json(); - } - - static async testConnection(node: NodeConfig): Promise<{ - status: string; - message?: string; - isCluster?: boolean; - nodeCount?: number; - clusterNodeCount?: number; - datastoreCount?: number; - }> { - const response = await fetch(`${this.baseUrl}/test-connection`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(node), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(error); - } - - return response.json(); - } - - static async testExistingNode(nodeId: string): Promise<{ - status: string; - message?: string; - latency?: number; - }> { - const response = await fetch(`${this.baseUrl}/${nodeId}/test`, { - method: 'POST', - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(error); - } - - return response.json(); - } -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/api/notifications.ts b/internal/api/frontend-modern/src/api/notifications.ts deleted file mode 100644 index 172559a49..000000000 --- a/internal/api/frontend-modern/src/api/notifications.ts +++ /dev/null @@ -1,231 +0,0 @@ - -export interface EmailProvider { - id?: string; - name: string; - smtpHost: string; - smtpPort: number; - tls: boolean; - startTLS: boolean; - authRequired: boolean; - instructions: string; - server?: string; - port?: number; - security?: 'none' | 'tls' | 'starttls'; -} - -export interface WebhookTemplate { - id?: string; - service: string; - name: string; - urlPattern: string; - method: string; - headers: Record; - payloadTemplate: string; - instructions: string; - description?: string; - template?: { - url?: string; - method?: string; - headers?: Record; - body?: string; - }; -} - -export interface EmailConfig { - enabled: boolean; - provider: string; - server: string; - port: number; - username: string; - password?: string; - from: string; - to: string[]; - tls: boolean; - startTLS: boolean; -} - -export interface Webhook { - id: string; - name: string; - url: string; - method: string; - headers: Record; - template?: string; - enabled: boolean; - service?: string; // Added to support Discord, Slack, etc. -} - -export interface NotificationTestRequest { - type: 'email' | 'webhook'; - config?: any; // Backend expects different format than frontend types - webhookId?: string; -} - -export class NotificationsAPI { - private static baseUrl = '/api/notifications'; - - // Email configuration - static async getEmailConfig(): Promise { - const response = await fetch(`${this.baseUrl}/email`); - if (!response.ok) { - throw new Error('Failed to fetch email configuration'); - } - const backendConfig = await response.json(); - - // Backend already returns fields with correct names (server, port) - return { - enabled: backendConfig.enabled || false, - provider: backendConfig.provider || '', - server: backendConfig.server || '', - port: backendConfig.port || 587, - username: backendConfig.username || '', - password: backendConfig.password || '', - from: backendConfig.from || '', - to: backendConfig.to || [], - tls: backendConfig.tls || false, - startTLS: backendConfig.startTLS || false - }; - } - - static async updateEmailConfig(config: EmailConfig): Promise<{ success: boolean }> { - // Backend expects fields with these names (server, port) - const backendConfig = { - enabled: config.enabled, - server: config.server, - port: config.port, - username: config.username, - password: config.password, - from: config.from, - to: config.to, - tls: config.tls || false, - startTLS: config.startTLS || false, - provider: config.provider || '' - }; - - const response = await fetch(`${this.baseUrl}/email`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(backendConfig), - }); - - if (!response.ok) { - throw new Error('Failed to update email configuration'); - } - - return response.json(); - } - - // Webhook management - static async getWebhooks(): Promise { - const response = await fetch(`${this.baseUrl}/webhooks`); - if (!response.ok) { - throw new Error('Failed to fetch webhooks'); - } - return response.json(); - } - - static async createWebhook(webhook: Omit): Promise { - const response = await fetch(`${this.baseUrl}/webhooks`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(webhook), - }); - - if (!response.ok) { - throw new Error('Failed to create webhook'); - } - - return response.json(); - } - - static async updateWebhook(id: string, webhook: Partial): Promise { - const response = await fetch(`${this.baseUrl}/webhooks/${id}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(webhook), - }); - - if (!response.ok) { - throw new Error('Failed to update webhook'); - } - - return response.json(); - } - - static async deleteWebhook(id: string): Promise<{ success: boolean }> { - const response = await fetch(`${this.baseUrl}/webhooks/${id}`, { - method: 'DELETE', - }); - - if (!response.ok) { - throw new Error('Failed to delete webhook'); - } - - return response.json(); - } - - // Templates and providers - static async getEmailProviders(): Promise { - const response = await fetch(`${this.baseUrl}/email-providers`); - if (!response.ok) { - throw new Error('Failed to fetch email providers'); - } - return response.json(); - } - - static async getWebhookTemplates(): Promise { - const response = await fetch(`${this.baseUrl}/webhook-templates`); - if (!response.ok) { - throw new Error('Failed to fetch webhook templates'); - } - return response.json(); - } - - // Testing - static async testNotification(request: NotificationTestRequest): Promise<{ success: boolean; message?: string }> { - const body: { method: string; config?: any } = { method: request.type }; - - // Include config if provided for testing without saving - if (request.config) { - body.config = request.config; - } - - const response = await fetch(`${this.baseUrl}/test`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(error || 'Failed to test notification'); - } - - return response.json(); - } - - static async testWebhook(webhook: Webhook): Promise<{ success: boolean; message?: string }> { - const response = await fetch(`${this.baseUrl}/webhooks/test`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(webhook), - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(error || 'Failed to test webhook'); - } - - return response.json(); - } -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/api/settings.ts b/internal/api/frontend-modern/src/api/settings.ts deleted file mode 100644 index 77c593818..000000000 --- a/internal/api/frontend-modern/src/api/settings.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { - SettingsResponse, - SettingsUpdateRequest -} from '@/types/settings'; - -// System settings type matching Go backend -export interface SystemSettingsUpdate { - pollingInterval: number; // in seconds - backendPort?: number; - frontendPort?: number; - allowedOrigins?: string; - connectionTimeout?: number; // in seconds - updateChannel?: string; - autoUpdateEnabled?: boolean; - autoUpdateCheckInterval?: number; // in hours - autoUpdateTime?: string; // HH:MM format -} - -// Response types -export interface ApiResponse { - success?: boolean; - status?: string; - message?: string; - data?: T; -} - -export class SettingsAPI { - private static baseUrl = '/api'; - - static async getSettings(): Promise { - const response = await fetch(`${this.baseUrl}/settings`); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Failed to fetch settings'); - } - - return response.json() as Promise; - } - - // Full settings update (legacy - avoid using) - static async updateSettings(settings: SettingsUpdateRequest): Promise { - const response = await fetch(`${this.baseUrl}/settings/update`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(settings), - }); - - if (!response.ok) { - throw new Error('Failed to update settings'); - } - - return response.json() as Promise; - } - - // System settings update (preferred) - static async updateSystemSettings(settings: SystemSettingsUpdate): Promise { - const response = await fetch(`${this.baseUrl}/config/system`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(settings), - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(errorText || 'Failed to update system settings'); - } - - return response.json() as Promise; - } - - static async validateSettings(settings: SettingsUpdateRequest): Promise { - const response = await fetch(`${this.baseUrl}/settings/validate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(settings), - }); - - if (!response.ok) { - throw new Error('Failed to validate settings'); - } - - return response.json() as Promise; - } -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/api/updates.ts b/internal/api/frontend-modern/src/api/updates.ts deleted file mode 100644 index 8637a418b..000000000 --- a/internal/api/frontend-modern/src/api/updates.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Remove apiRequest import - use fetch directly - -export interface UpdateInfo { - available: boolean; - currentVersion: string; - latestVersion: string; - releaseNotes: string; - releaseDate: string; - downloadUrl: string; - isPrerelease: boolean; -} - -export interface UpdateStatus { - status: string; - progress: number; - message: string; - error?: string; - updatedAt: string; -} - -export interface VersionInfo { - version: string; - build: string; - runtime: string; - channel?: string; - isDocker: boolean; - isDevelopment: boolean; -} - -export class UpdatesAPI { - static async checkForUpdates(channel?: string): Promise { - const url = channel ? `/api/updates/check?channel=${channel}` : '/api/updates/check'; - const response = await fetch(url); - if (!response.ok) { - throw new Error('Failed to check for updates'); - } - return response.json(); - } - - static async applyUpdate(downloadUrl: string): Promise<{ status: string; message: string }> { - const response = await fetch('/api/updates/apply', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ downloadUrl }), - }); - if (!response.ok) { - throw new Error('Failed to apply update'); - } - return response.json(); - } - - static async getUpdateStatus(): Promise { - const response = await fetch('/api/updates/status'); - if (!response.ok) { - throw new Error('Failed to get update status'); - } - return response.json(); - } - - static async getVersion(): Promise { - const response = await fetch('/api/version'); - if (!response.ok) { - throw new Error('Failed to get version'); - } - return response.json(); - } -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/components/Alerts/CustomRulesTab.tsx b/internal/api/frontend-modern/src/components/Alerts/CustomRulesTab.tsx deleted file mode 100644 index 427ef96ed..000000000 --- a/internal/api/frontend-modern/src/components/Alerts/CustomRulesTab.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { For, Show } from 'solid-js'; -import type { CustomAlertRule } from '@/types/alerts'; - -interface CustomRulesTabProps { - rules: CustomAlertRule[]; - onUpdateRules: (rules: CustomAlertRule[]) => void; - onHasChanges: (hasChanges: boolean) => void; -} - -export function CustomRulesTab(props: CustomRulesTabProps) { - - const deleteRule = (ruleId: string) => { - 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 - ); - 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} `); - }; - - const getThresholdsSummary = (rule: CustomAlertRule): string => { - const parts: string[] = []; - if (rule.thresholds.cpu !== undefined) parts.push(`CPU: ${rule.thresholds.cpu}%`); - if (rule.thresholds.memory !== undefined) parts.push(`Memory: ${rule.thresholds.memory}%`); - if (rule.thresholds.disk !== undefined) parts.push(`Disk: ${rule.thresholds.disk}%`); - return parts.length > 0 ? parts.join(', ') : 'Using global defaults'; - }; - - return ( -
- {/* Header */} -
-

Custom Alert Rules

-

- Custom rules apply specific thresholds to guests matching filter conditions. - Rules are evaluated in priority order (higher number = higher priority). -

-
- - {/* Priority Order Explanation */} -
-
- - - -
-

Alert Priority Order:

-
    -
  1. Guest-specific overrides (highest priority)
  2. -
  3. Custom rules (evaluated by priority number)
  4. -
  5. Global default thresholds (lowest priority)
  6. -
-
-
-
- - {/* Rules List */} - 0} fallback={ -
- - - -

- No custom alert rules defined yet. Create rules from the Dashboard by applying filters and clicking "Create Alert". -

-
- }> -
- b.priority - a.priority)}> - {(rule) => ( -
-
-
-
-
-

{rule.name}

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

{rule.description}

-
-
-
- - -
-
- -
-
- Filters: -
- - {getFilterDescription(rule)} - -
-
- -
- Thresholds: - - {getThresholdsSummary(rule)} - -
- - -
- Notify: -
- - - Email - - - - - Webhook - - -
-
-
-
-
-
- )} -
-
-
-
- ); -} \ No newline at end of file diff --git a/internal/api/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx b/internal/api/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx deleted file mode 100644 index f19ae63a8..000000000 --- a/internal/api/frontend-modern/src/components/Alerts/EmailProviderSelect.tsx +++ /dev/null @@ -1,332 +0,0 @@ -import { createSignal, createEffect, Show, For } from 'solid-js'; -import { NotificationsAPI } from '@/api/notifications'; - -interface EmailProvider { - name: string; - smtpHost: string; - smtpPort: number; - tls: boolean; - startTLS: boolean; - authRequired: boolean; - instructions: string; -} - -interface EmailConfig { - enabled: boolean; - provider: string; - server: string; // Fixed: use 'server' not 'smtpHost' - port: number; // Fixed: use 'port' not 'smtpPort' - from: string; - username: string; - password: string; - to: string[]; - tls: boolean; - startTLS: boolean; - replyTo: string; - maxRetries: number; - retryDelay: number; - rateLimit: number; -} - -interface EmailProviderSelectProps { - config: EmailConfig; - onChange: (config: EmailConfig) => void; - onTest: () => void; - testing?: boolean; -} - -export function EmailProviderSelect(props: EmailProviderSelectProps) { - const [providers, setProviders] = createSignal([]); - const [showProviders, setShowProviders] = createSignal(false); - const [showAdvanced, setShowAdvanced] = createSignal(false); - - // Load email providers - createEffect(async () => { - try { - const data = await NotificationsAPI.getEmailProviders(); - setProviders(data); - } catch (err) { - console.error('Failed to load email providers:', err); - } - }); - - const selectProvider = (provider: EmailProvider) => { - props.onChange({ - ...props.config, - provider: provider.name, - server: provider.smtpHost, // Fixed: use 'server' not 'smtpHost' - port: provider.smtpPort, // Fixed: use 'port' not 'smtpPort' - tls: provider.tls, - startTLS: provider.startTLS, - username: provider.name === 'SendGrid' ? 'apikey' : props.config.username, - }); - setShowProviders(false); - }; - - const currentProvider = () => providers().find(p => p.name === props.config.provider); - - return ( -
- {/* Provider Selection */} -
-
- - -
- - -
- - {(provider) => ( - - )} - -
-
- - -
-

- Setup Instructions -

-
-              {currentProvider()!.instructions}
-            
-
-
-
- - {/* Basic Configuration */} -
-
- - props.onChange({ ...props.config, server: e.currentTarget.value })} - placeholder="smtp.example.com" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" - /> -
- -
- - props.onChange({ ...props.config, port: parseInt(e.currentTarget.value) || 587 })} - placeholder="587" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" - /> -
- -
- - props.onChange({ ...props.config, from: e.currentTarget.value })} - placeholder="noreply@example.com" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" - /> -
- -
- - props.onChange({ ...props.config, replyTo: e.currentTarget.value })} - placeholder="admin@example.com (optional)" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" - /> -
- -
- - props.onChange({ ...props.config, username: e.currentTarget.value })} - placeholder={props.config.provider === 'SendGrid' ? 'apikey' : 'username@example.com'} - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" - /> -
- -
- - props.onChange({ ...props.config, password: e.currentTarget.value })} - placeholder="••••••••" - class="w-full px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" - /> -
-
- - {/* Recipients */} -
- -