diff --git a/frontend-modern/src/api/notifications.ts b/frontend-modern/src/api/notifications.ts index 3ef3f0370..c30c0a3f3 100644 --- a/frontend-modern/src/api/notifications.ts +++ b/frontend-modern/src/api/notifications.ts @@ -56,7 +56,7 @@ export interface Webhook { export interface NotificationTestRequest { type: 'email' | 'webhook'; - config?: EmailConfig | Webhook; + config?: any; // Backend expects different format than frontend types webhookId?: string; } @@ -186,7 +186,7 @@ export class NotificationsAPI { // Testing static async testNotification(request: NotificationTestRequest): Promise<{ success: boolean; message?: string }> { - const body: { method: string; config?: EmailConfig | Webhook } = { method: request.type }; + const body: { method: string; config?: any } = { method: request.type }; // Include config if provided for testing without saving if (request.config) { diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index a4d00af18..f1400926d 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -1372,22 +1372,20 @@ function DestinationsTab(props: DestinationsTabProps) { // Get current form values and convert to backend format const currentConfig = props.emailConfig(); - // If no recipients specified, use the from address as default recipient + // Keep recipients empty if none specified - backend will use from address const recipients = currentConfig.to && currentConfig.to.length > 0 ? currentConfig.to - : (currentConfig.from ? [currentConfig.from] : []); + : []; - const configToTest: EmailConfig = { + const configToTest = { enabled: currentConfig.enabled, - provider: currentConfig.provider, - server: currentConfig.smtpHost, - port: currentConfig.smtpPort, + smtpHost: currentConfig.smtpHost, + smtpPort: currentConfig.smtpPort, username: currentConfig.username, password: currentConfig.password, from: currentConfig.from, to: recipients, - tls: currentConfig.tls, - starttls: currentConfig.startTLS + tls: currentConfig.tls }; await NotificationsAPI.testNotification({ diff --git a/internal/api/notifications.go b/internal/api/notifications.go index ff86279ba..fa87d7779 100644 --- a/internal/api/notifications.go +++ b/internal/api/notifications.go @@ -179,12 +179,23 @@ func (h *NotificationHandlers) DeleteWebhook(w http.ResponseWriter, r *http.Requ // TestNotification sends a test notification func (h *NotificationHandlers) TestNotification(w http.ResponseWriter, r *http.Request) { + // Read body for debugging + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + log.Info(). + Str("body", string(body)). + Msg("Test notification request received") + var req struct { Method string `json:"method"` // "email" or "webhook" Config *notifications.EmailConfig `json:"config,omitempty"` // Optional config for testing } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + if err := json.Unmarshal(body, &req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -206,6 +217,15 @@ func (h *NotificationHandlers) TestNotification(w http.ResponseWriter, r *http.R // If config is provided, use it for testing (without saving) if req.Method == "email" && req.Config != nil { + log.Info(). + Bool("enabled", req.Config.Enabled). + Str("smtp", req.Config.SMTPHost). + Str("from", req.Config.From). + Int("toCount", len(req.Config.To)). + Strs("to", req.Config.To). + Bool("hasPassword", req.Config.Password != ""). + Msg("Testing email with provided config") + if err := h.monitor.GetNotificationManager().SendTestNotificationWithConfig(req.Method, req.Config, nodeInfo); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return diff --git a/testing-tools/check-ui-request.js b/testing-tools/check-ui-request.js new file mode 100644 index 000000000..3b6eab187 --- /dev/null +++ b/testing-tools/check-ui-request.js @@ -0,0 +1,51 @@ +const axios = require('axios'); +const { spawn } = require('child_process'); + +async function checkUIRequest() { + console.log('=== CHECKING UI REQUEST ===\n'); + + console.log('Starting log monitor...'); + const logProcess = spawn('sudo', ['tail', '-f', '/opt/pulse/pulse.log']); + + let capturedLogs = ''; + + logProcess.stdout.on('data', (data) => { + const output = data.toString(); + capturedLogs += output; + if (output.includes('Test notification request') || output.includes('Testing email with provided config')) { + process.stdout.write(output); + } + }); + + // Wait for log monitor to start + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log('\nPlease click "Send Test Email" in the UI now...\n'); + console.log('Waiting for request (30 seconds)...\n'); + + // Wait for user to trigger the test + await new Promise(resolve => setTimeout(resolve, 30000)); + + logProcess.kill(); + + // Check if we captured the request + if (capturedLogs.includes('Test notification request')) { + console.log('\n✅ Found test request in logs'); + + // Extract the request body + const bodyMatch = capturedLogs.match(/body=({.*?}) msg=/); + if (bodyMatch) { + try { + const body = JSON.parse(bodyMatch[1]); + console.log('\nRequest body:', JSON.stringify(body, null, 2)); + } catch (e) { + console.log('Could not parse body'); + } + } + } else { + console.log('\n❌ No test request found in logs'); + console.log('Make sure you clicked "Send Test Email" in the UI'); + } +} + +checkUIRequest().catch(console.error); \ No newline at end of file diff --git a/testing-tools/intercept-ui-request.js b/testing-tools/intercept-ui-request.js new file mode 100644 index 000000000..062ff5966 --- /dev/null +++ b/testing-tools/intercept-ui-request.js @@ -0,0 +1,37 @@ +const express = require('express'); +const app = express(); + +app.use(express.json()); + +// Intercept test requests +app.post('/api/notifications/test', (req, res) => { + console.log('\n=== TEST EMAIL REQUEST ==='); + console.log('Headers:', req.headers); + console.log('\nBody:', JSON.stringify(req.body, null, 2)); + + if (req.body.config) { + console.log('\nConfig details:'); + console.log(' From:', req.body.config.from); + console.log(' To:', req.body.config.to); + console.log(' Recipients count:', req.body.config.to ? req.body.config.to.length : 0); + console.log(' Has password:', !!req.body.config.password); + console.log(' SMTP Host:', req.body.config.server); + console.log(' SMTP Port:', req.body.config.port); + } + + // Return error to see what frontend shows + res.status(400).send('Test intercept - check console output'); +}); + +// Proxy other requests to backend +app.use((req, res) => { + console.log('Proxying:', req.method, req.url); + res.status(404).send('Not implemented in test server'); +}); + +const PORT = 3001; +app.listen(PORT, () => { + console.log(`Test server listening on port ${PORT}`); + console.log('Update frontend to use http://localhost:3001 temporarily'); + console.log('Or use browser dev tools to intercept the request'); +}); \ No newline at end of file diff --git a/testing-tools/test-frontend-payload.js b/testing-tools/test-frontend-payload.js new file mode 100644 index 000000000..92384817c --- /dev/null +++ b/testing-tools/test-frontend-payload.js @@ -0,0 +1,84 @@ +const { chromium } = require('playwright'); + +const FRONTEND_URL = 'http://192.168.0.123:7655'; + +async function testFrontendPayload() { + console.log('=== TESTING FRONTEND EMAIL PAYLOAD ===\n'); + + const browser = await chromium.launch({ + headless: true + }); + + try { + const context = await browser.newContext(); + const page = await context.newPage(); + + // Intercept the API request to see what's being sent + page.on('request', request => { + if (request.url().includes('/api/notifications/test')) { + console.log('Test email request intercepted:'); + console.log(' URL:', request.url()); + console.log(' Method:', request.method()); + console.log(' Headers:', request.headers()); + console.log(' Body:', request.postData()); + + if (request.postData()) { + try { + const body = JSON.parse(request.postData()); + console.log('\nParsed body:'); + console.log(JSON.stringify(body, null, 2)); + + if (body.config) { + console.log('\nEmail config details:'); + console.log(' From:', body.config.from); + console.log(' To:', body.config.to); + console.log(' To length:', body.config.to ? body.config.to.length : 0); + console.log(' Has password:', !!body.config.password); + } + } catch (e) { + console.log('Could not parse body as JSON'); + } + } + } + }); + + // Monitor responses + page.on('response', response => { + if (response.url().includes('/api/notifications/test')) { + console.log('\nTest email response:'); + console.log(' Status:', response.status()); + response.text().then(text => { + console.log(' Body:', text); + }); + } + }); + + // Navigate to alerts page + console.log('1. Navigating to alerts page...'); + await page.goto(`${FRONTEND_URL}/alerts`); + await page.waitForTimeout(2000); + + // Click on Notifications tab + console.log('2. Clicking Notifications tab...'); + await page.click('button:has-text("Notifications")'); + await page.waitForTimeout(1000); + + // Click Send Test Email + console.log('3. Clicking Send Test Email...\n'); + await page.click('button:has-text("Send Test Email")'); + + // Wait for request/response + await page.waitForTimeout(3000); + + } catch (error) { + console.error('Error:', error.message); + } finally { + await browser.close(); + } +} + +if (require.main === module) { + testFrontendPayload().catch(console.error); +} + +module.exports = { testFrontendPayload }; \ No newline at end of file diff --git a/testing-tools/test-with-config.js b/testing-tools/test-with-config.js new file mode 100644 index 000000000..7cfe0ba3a --- /dev/null +++ b/testing-tools/test-with-config.js @@ -0,0 +1,70 @@ +const axios = require('axios'); + +async function testWithConfig() { + console.log('=== TESTING EMAIL WITH EXPLICIT CONFIG ===\n'); + + // Test 1: With empty recipients array + console.log('Test 1: Empty recipients array'); + try { + const response1 = await axios.post('http://localhost:3000/api/notifications/test', { + method: 'email', + config: { + enabled: true, + smtpHost: 'smtp.gmail.com', + smtpPort: 587, + username: 'courtmanr@gmail.com', + password: 'zlff ruyk bxxf cxch', + from: 'courtmanr@gmail.com', + to: [], // Empty array + tls: true + } + }); + console.log('✅ Success:', response1.data); + } catch (error) { + console.error('❌ Failed:', error.response?.data || error.message); + } + + // Test 2: With no password + console.log('\nTest 2: No password (should fail)'); + try { + const response2 = await axios.post('http://localhost:3000/api/notifications/test', { + method: 'email', + config: { + enabled: true, + smtpHost: 'smtp.gmail.com', + smtpPort: 587, + username: 'courtmanr@gmail.com', + password: '', // Empty password + from: 'courtmanr@gmail.com', + to: [], + tls: true + } + }); + console.log('✅ Success:', response2.data); + } catch (error) { + console.error('❌ Failed:', error.response?.data || error.message); + } + + // Test 3: Check what frontend sends (server vs smtpHost) + console.log('\nTest 3: Using "server" field like frontend'); + try { + const response3 = await axios.post('http://localhost:3000/api/notifications/test', { + method: 'email', + config: { + enabled: true, + server: 'smtp.gmail.com', // Frontend uses 'server' + port: 587, + username: 'courtmanr@gmail.com', + password: 'zlff ruyk bxxf cxch', + from: 'courtmanr@gmail.com', + to: [], + tls: true + } + }); + console.log('✅ Success:', response3.data); + } catch (error) { + console.error('❌ Failed:', error.response?.data || error.message); + } +} + +testWithConfig().catch(console.error); \ No newline at end of file