diff --git a/backend/internal/api/handlers/proxy_host_handler.go b/backend/internal/api/handlers/proxy_host_handler.go index 2f70fa49..9807c820 100644 --- a/backend/internal/api/handlers/proxy_host_handler.go +++ b/backend/internal/api/handlers/proxy_host_handler.go @@ -297,6 +297,14 @@ func (h *ProxyHostHandler) Update(c *gin.Context) { } } + // Sync associated uptime monitor with updated proxy host values + if h.uptimeService != nil { + if err := h.uptimeService.SyncMonitorForHost(host.ID); err != nil { + middleware.GetRequestLogger(c).WithError(err).WithField("host_id", host.ID).Warn("Failed to sync uptime monitor for host") + // Don't fail the request if sync fails - the host update succeeded + } + } + c.JSON(http.StatusOK, host) } diff --git a/backend/internal/services/uptime_service.go b/backend/internal/services/uptime_service.go index e9a222b5..e39d013d 100644 --- a/backend/internal/services/uptime_service.go +++ b/backend/internal/services/uptime_service.go @@ -3,8 +3,8 @@ package services import ( "context" "encoding/json" + "errors" "fmt" - "github.com/Wikid82/charon/backend/internal/logger" "net" "net/http" "net/url" @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/Wikid82/charon/backend/internal/logger" "github.com/Wikid82/charon/backend/internal/models" "github.com/Wikid82/charon/backend/internal/util" "gorm.io/gorm" @@ -806,6 +807,47 @@ func (s *UptimeService) FlushPendingNotifications() { } } +// SyncMonitorForHost updates the uptime monitor linked to a specific proxy host. +// This should be called when a proxy host is edited to keep the monitor in sync. +// Returns nil if no monitor exists for the host (does not create one). +func (s *UptimeService) SyncMonitorForHost(hostID uint) error { + var host models.ProxyHost + if err := s.DB.First(&host, hostID).Error; err != nil { + return err + } + + var monitor models.UptimeMonitor + if err := s.DB.Where("proxy_host_id = ?", hostID).First(&monitor).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // No monitor to sync + } + return err + } + + // Update monitor fields based on current proxy host values + domains := strings.Split(host.DomainNames, ",") + firstDomain := "" + if len(domains) > 0 { + firstDomain = strings.TrimSpace(domains[0]) + } + + scheme := "http" + if host.SSLForced { + scheme = "https" + } + + newName := host.Name + if newName == "" { + newName = firstDomain + } + + monitor.Name = newName + monitor.URL = fmt.Sprintf("%s://%s", scheme, firstDomain) + monitor.UpstreamHost = host.ForwardHost + + return s.DB.Save(&monitor).Error +} + // CRUD for Monitors func (s *UptimeService) ListMonitors() ([]models.UptimeMonitor, error) { diff --git a/backend/internal/services/uptime_service_test.go b/backend/internal/services/uptime_service_test.go index 0ff0d262..5fa85341 100644 --- a/backend/internal/services/uptime_service_test.go +++ b/backend/internal/services/uptime_service_test.go @@ -1201,3 +1201,156 @@ func TestFormatDuration(t *testing.T) { assert.Equal(t, tc.expected, result, "formatDuration(%v)", tc.input) } } + +func TestUptimeService_SyncMonitorForHost(t *testing.T) { + t.Run("updates monitor when proxy host is edited", func(t *testing.T) { + db := setupUptimeTestDB(t) + ns := NewNotificationService(db) + us := NewUptimeService(db, ns) + + // Create a proxy host + host := models.ProxyHost{ + UUID: "sync-test-1", + Name: "Original Name", + DomainNames: "original.example.com", + ForwardHost: "10.0.0.1", + ForwardPort: 8080, + SSLForced: false, + Enabled: true, + } + db.Create(&host) + + // Sync monitors to create the uptime monitor + err := us.SyncMonitors() + assert.NoError(t, err) + + // Verify monitor was created with original values + var monitor models.UptimeMonitor + err = db.Where("proxy_host_id = ?", host.ID).First(&monitor).Error + assert.NoError(t, err) + assert.Equal(t, "Original Name", monitor.Name) + assert.Equal(t, "http://original.example.com", monitor.URL) + assert.Equal(t, "10.0.0.1", monitor.UpstreamHost) + + // Update the proxy host + host.Name = "Updated Name" + host.DomainNames = "updated.example.com" + host.ForwardHost = "10.0.0.2" + host.SSLForced = true + db.Save(&host) + + // Call SyncMonitorForHost + err = us.SyncMonitorForHost(host.ID) + assert.NoError(t, err) + + // Verify monitor was updated + err = db.Where("proxy_host_id = ?", host.ID).First(&monitor).Error + assert.NoError(t, err) + assert.Equal(t, "Updated Name", monitor.Name) + assert.Equal(t, "https://updated.example.com", monitor.URL) + assert.Equal(t, "10.0.0.2", monitor.UpstreamHost) + }) + + t.Run("returns nil when no monitor exists", func(t *testing.T) { + db := setupUptimeTestDB(t) + ns := NewNotificationService(db) + us := NewUptimeService(db, ns) + + // Create a proxy host without creating a monitor + host := models.ProxyHost{ + UUID: "no-monitor-test", + Name: "No Monitor Host", + DomainNames: "nomonitor.example.com", + ForwardHost: "10.0.0.3", + ForwardPort: 8080, + Enabled: true, + } + db.Create(&host) + + // Call SyncMonitorForHost - should return nil without error + err := us.SyncMonitorForHost(host.ID) + assert.NoError(t, err) + + // Verify no monitor was created + var count int64 + db.Model(&models.UptimeMonitor{}).Where("proxy_host_id = ?", host.ID).Count(&count) + assert.Equal(t, int64(0), count) + }) + + t.Run("returns error when host does not exist", func(t *testing.T) { + db := setupUptimeTestDB(t) + ns := NewNotificationService(db) + us := NewUptimeService(db, ns) + + // Call SyncMonitorForHost with non-existent host ID + err := us.SyncMonitorForHost(99999) + assert.Error(t, err) + }) + + t.Run("uses domain name when proxy host name is empty", func(t *testing.T) { + db := setupUptimeTestDB(t) + ns := NewNotificationService(db) + us := NewUptimeService(db, ns) + + // Create a proxy host with a name + host := models.ProxyHost{ + UUID: "empty-name-test", + Name: "Has Name", + DomainNames: "domain.example.com", + ForwardHost: "10.0.0.4", + ForwardPort: 8080, + Enabled: true, + } + db.Create(&host) + + // Sync monitors + err := us.SyncMonitors() + assert.NoError(t, err) + + // Clear the host name + host.Name = "" + db.Save(&host) + + // Call SyncMonitorForHost + err = us.SyncMonitorForHost(host.ID) + assert.NoError(t, err) + + // Verify monitor uses domain name + var monitor models.UptimeMonitor + err = db.Where("proxy_host_id = ?", host.ID).First(&monitor).Error + assert.NoError(t, err) + assert.Equal(t, "domain.example.com", monitor.Name) + }) + + t.Run("handles multiple domains correctly", func(t *testing.T) { + db := setupUptimeTestDB(t) + ns := NewNotificationService(db) + us := NewUptimeService(db, ns) + + // Create a proxy host with multiple domains + host := models.ProxyHost{ + UUID: "multi-domain-test", + Name: "Multi Domain", + DomainNames: "first.example.com, second.example.com, third.example.com", + ForwardHost: "10.0.0.5", + ForwardPort: 8080, + SSLForced: true, + Enabled: true, + } + db.Create(&host) + + // Sync monitors + err := us.SyncMonitors() + assert.NoError(t, err) + + // Call SyncMonitorForHost + err = us.SyncMonitorForHost(host.ID) + assert.NoError(t, err) + + // Verify monitor uses first domain + var monitor models.UptimeMonitor + err = db.Where("proxy_host_id = ?", host.ID).First(&monitor).Error + assert.NoError(t, err) + assert.Equal(t, "https://first.example.com", monitor.URL) + }) +} diff --git a/docs/plans/ui_ux_bugfixes_spec.md b/docs/plans/ui_ux_bugfixes_spec.md index 331c9c23..6b9e9910 100644 --- a/docs/plans/ui_ux_bugfixes_spec.md +++ b/docs/plans/ui_ux_bugfixes_spec.md @@ -294,7 +294,7 @@ if (!status) return
+
+
-
-
+
) : (
-
+
)}