diff --git a/backend/internal/api/handlers/docker_handler.go b/backend/internal/api/handlers/docker_handler.go index 95db8cb7..487decef 100644 --- a/backend/internal/api/handlers/docker_handler.go +++ b/backend/internal/api/handlers/docker_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "net/http" "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" @@ -8,11 +9,15 @@ import ( ) type DockerHandler struct { - dockerService *services.DockerService + dockerService *services.DockerService + remoteServerService *services.RemoteServerService } -func NewDockerHandler(dockerService *services.DockerService) *DockerHandler { - return &DockerHandler{dockerService: dockerService} +func NewDockerHandler(dockerService *services.DockerService, remoteServerService *services.RemoteServerService) *DockerHandler { + return &DockerHandler{ + dockerService: dockerService, + remoteServerService: remoteServerService, + } } func (h *DockerHandler) RegisterRoutes(r *gin.RouterGroup) { @@ -21,6 +26,22 @@ func (h *DockerHandler) RegisterRoutes(r *gin.RouterGroup) { func (h *DockerHandler) ListContainers(c *gin.Context) { host := c.Query("host") + serverID := c.Query("server_id") + + // If server_id is provided, look up the remote server + if serverID != "" { + server, err := h.remoteServerService.GetByUUID(serverID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Remote server not found"}) + return + } + + // Construct Docker host string + // Assuming TCP for now as that's what RemoteServer supports (Host/Port) + // TODO: Support SSH if/when RemoteServer supports it + host = fmt.Sprintf("tcp://%s:%d", server.Host, server.Port) + } + containers, err := h.dockerService.ListContainers(c.Request.Context(), host) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list containers: " + err.Error()}) diff --git a/backend/internal/api/handlers/docker_handler_test.go b/backend/internal/api/handlers/docker_handler_test.go index 36b2f5bd..7f560100 100644 --- a/backend/internal/api/handlers/docker_handler_test.go +++ b/backend/internal/api/handlers/docker_handler_test.go @@ -5,9 +5,13 @@ import ( "net/http/httptest" "testing" + "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestDockerHandler_ListContainers(t *testing.T) { @@ -26,7 +30,15 @@ func TestDockerHandler_ListContainers(t *testing.T) { t.Skip("Docker not available") } - h := NewDockerHandler(svc) + // Setup DB for RemoteServerService + dsn := "file:" + t.Name() + "?mode=memory&cache=shared" + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + require.NoError(t, err) + require.NoError(t, db.AutoMigrate(&models.RemoteServer{})) + + rsService := services.NewRemoteServerService(db) + + h := NewDockerHandler(svc, rsService) gin.SetMode(gin.TestMode) r := gin.New() h.RegisterRoutes(r.Group("/")) diff --git a/backend/internal/api/handlers/handlers_test.go b/backend/internal/api/handlers/handlers_test.go index 5ac9d79d..c35ae4f2 100644 --- a/backend/internal/api/handlers/handlers_test.go +++ b/backend/internal/api/handlers/handlers_test.go @@ -52,7 +52,7 @@ func TestRemoteServerHandler_List(t *testing.T) { db.Create(server) ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) @@ -75,7 +75,7 @@ func TestRemoteServerHandler_Create(t *testing.T) { db := setupTestDB() ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) @@ -119,7 +119,7 @@ func TestRemoteServerHandler_TestConnection(t *testing.T) { db.Create(server) ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) @@ -153,7 +153,7 @@ func TestRemoteServerHandler_Get(t *testing.T) { db.Create(server) ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) @@ -186,7 +186,7 @@ func TestRemoteServerHandler_Update(t *testing.T) { db.Create(server) ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) @@ -231,7 +231,7 @@ func TestRemoteServerHandler_Delete(t *testing.T) { db.Create(server) ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) @@ -343,7 +343,7 @@ func TestRemoteServerHandler_Errors(t *testing.T) { db := setupTestDB() ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) router := gin.New() handler.RegisterRoutes(router.Group("/api/v1")) diff --git a/backend/internal/api/handlers/remote_server_handler.go b/backend/internal/api/handlers/remote_server_handler.go index 83625fd1..10e9a4b9 100644 --- a/backend/internal/api/handlers/remote_server_handler.go +++ b/backend/internal/api/handlers/remote_server_handler.go @@ -8,7 +8,6 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" - "gorm.io/gorm" "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models" "github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/services" @@ -21,9 +20,9 @@ type RemoteServerHandler struct { } // NewRemoteServerHandler creates a new remote server handler. -func NewRemoteServerHandler(db *gorm.DB, ns *services.NotificationService) *RemoteServerHandler { +func NewRemoteServerHandler(service *services.RemoteServerService, ns *services.NotificationService) *RemoteServerHandler { return &RemoteServerHandler{ - service: services.NewRemoteServerService(db), + service: service, notificationService: ns, } } diff --git a/backend/internal/api/handlers/remote_server_handler_test.go b/backend/internal/api/handlers/remote_server_handler_test.go index 5ea05f4d..1a26cc8d 100644 --- a/backend/internal/api/handlers/remote_server_handler_test.go +++ b/backend/internal/api/handlers/remote_server_handler_test.go @@ -23,7 +23,7 @@ func setupRemoteServerTest_New(t *testing.T) (*gin.Engine, *handlers.RemoteServe db.AutoMigrate(&models.RemoteServer{}) ns := services.NewNotificationService(db) - handler := handlers.NewRemoteServerHandler(db, ns) + handler := handlers.NewRemoteServerHandler(services.NewRemoteServerService(db), ns) r := gin.Default() api := r.Group("/api/v1") diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 3be1c526..890b9c9b 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -73,6 +73,9 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { // Notification Service (needed for multiple handlers) notificationService := services.NewNotificationService(db) + // Remote Server Service (needed for Docker handler) + remoteServerService := services.NewRemoteServerService(db) + api.POST("/auth/login", authHandler.Login) api.POST("/auth/register", authHandler.Register) @@ -126,7 +129,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { // Docker dockerService, err := services.NewDockerService() if err == nil { // Only register if Docker is available - dockerHandler := handlers.NewDockerHandler(dockerService) + dockerHandler := handlers.NewDockerHandler(dockerService, remoteServerService) dockerHandler.RegisterRoutes(protected) } else { fmt.Printf("Warning: Docker service unavailable: %v\n", err) @@ -176,7 +179,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { proxyHostHandler := handlers.NewProxyHostHandler(db, caddyManager, notificationService) proxyHostHandler.RegisterRoutes(api) - remoteServerHandler := handlers.NewRemoteServerHandler(db, notificationService) + remoteServerHandler := handlers.NewRemoteServerHandler(remoteServerService, notificationService) remoteServerHandler.RegisterRoutes(api) userHandler := handlers.NewUserHandler(db) diff --git a/backend/internal/models/uptime.go b/backend/internal/models/uptime.go index 2cb2490f..c0211d15 100644 --- a/backend/internal/models/uptime.go +++ b/backend/internal/models/uptime.go @@ -8,15 +8,16 @@ import ( ) type UptimeMonitor struct { - ID string `gorm:"primaryKey" json:"id"` - ProxyHostID *uint `json:"proxy_host_id"` // Optional link to proxy host - Name string `json:"name"` - Type string `json:"type"` // http, tcp, ping - URL string `json:"url"` - Interval int `json:"interval"` // seconds - Enabled bool `json:"enabled"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `gorm:"primaryKey" json:"id"` + ProxyHostID *uint `json:"proxy_host_id"` // Optional link to proxy host + RemoteServerID *uint `json:"remote_server_id"` // Optional link to remote server + Name string `json:"name"` + Type string `json:"type"` // http, tcp, ping + URL string `json:"url"` + Interval int `json:"interval"` // seconds + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` // Current Status (Cached) Status string `json:"status"` // up, down, maintenance, pending diff --git a/backend/internal/services/uptime_service.go b/backend/internal/services/uptime_service.go index eb85304e..20527f59 100644 --- a/backend/internal/services/uptime_service.go +++ b/backend/internal/services/uptime_service.go @@ -98,6 +98,56 @@ func (s *UptimeService) SyncMonitors() error { } } } + + // Sync Remote Servers + var remoteServers []models.RemoteServer + if err := s.DB.Find(&remoteServers).Error; err != nil { + return err + } + + for _, server := range remoteServers { + var monitor models.UptimeMonitor + err := s.DB.Where("remote_server_id = ?", server.ID).First(&monitor).Error + + targetType := "tcp" + targetURL := fmt.Sprintf("%s:%d", server.Host, server.Port) + + if server.Scheme == "http" || server.Scheme == "https" { + targetType = server.Scheme + targetURL = fmt.Sprintf("%s://%s:%d", server.Scheme, server.Host, server.Port) + } + + switch err { + case gorm.ErrRecordNotFound: + monitor = models.UptimeMonitor{ + RemoteServerID: &server.ID, + Name: server.Name, + Type: targetType, + URL: targetURL, + Interval: 60, + Enabled: server.Enabled, + Status: "pending", + } + if err := s.DB.Create(&monitor).Error; err != nil { + log.Printf("Failed to create monitor for remote server %d: %v", server.ID, err) + } + case nil: + if monitor.Name != server.Name { + monitor.Name = server.Name + s.DB.Save(&monitor) + } + if monitor.URL != targetURL || monitor.Type != targetType { + monitor.URL = targetURL + monitor.Type = targetType + s.DB.Save(&monitor) + } + if monitor.Enabled != server.Enabled { + monitor.Enabled = server.Enabled + s.DB.Save(&monitor) + } + } + } + return nil } diff --git a/backend/internal/services/uptime_service_test.go b/backend/internal/services/uptime_service_test.go index 92c854d8..ac43762b 100644 --- a/backend/internal/services/uptime_service_test.go +++ b/backend/internal/services/uptime_service_test.go @@ -20,7 +20,7 @@ func setupUptimeTestDB(t *testing.T) *gorm.DB { if err != nil { t.Fatalf("Failed to connect to database: %v", err) } - err = db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}, &models.Setting{}, &models.ProxyHost{}, &models.UptimeMonitor{}, &models.UptimeHeartbeat{}) + err = db.AutoMigrate(&models.Notification{}, &models.NotificationProvider{}, &models.Setting{}, &models.ProxyHost{}, &models.UptimeMonitor{}, &models.UptimeHeartbeat{}, &models.RemoteServer{}) if err != nil { t.Fatalf("Failed to migrate database: %v", err) } diff --git a/frontend/src/api/docker.ts b/frontend/src/api/docker.ts index cf5e8634..5a194cb0 100644 --- a/frontend/src/api/docker.ts +++ b/frontend/src/api/docker.ts @@ -18,8 +18,11 @@ export interface DockerContainer { } export const dockerApi = { - listContainers: async (host?: string): Promise => { - const params = host ? { host } : undefined + listContainers: async (host?: string, serverId?: string): Promise => { + const params: Record = {} + if (host) params.host = host + if (serverId) params.server_id = serverId + const response = await client.get('/docker/containers', { params }) return response.data }, diff --git a/frontend/src/api/uptime.ts b/frontend/src/api/uptime.ts index ef4deda7..bdee342d 100644 --- a/frontend/src/api/uptime.ts +++ b/frontend/src/api/uptime.ts @@ -2,6 +2,8 @@ import client from './client'; export interface UptimeMonitor { id: string; + proxy_host_id?: number; + remote_server_id?: number; name: string; type: string; url: string; diff --git a/frontend/src/components/ProxyHostForm.tsx b/frontend/src/components/ProxyHostForm.tsx index 36087259..2c195adb 100644 --- a/frontend/src/components/ProxyHostForm.tsx +++ b/frontend/src/components/ProxyHostForm.tsx @@ -35,11 +35,14 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor const { servers: remoteServers } = useRemoteServers() const { domains, createDomain } = useDomains() const { certificates } = useCertificates() - const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker( - formData.forward_host ? undefined : undefined // Simplified for now, logic below handles it - ) const [connectionSource, setConnectionSource] = useState<'local' | 'custom' | string>('custom') + + const { containers: dockerContainers, isLoading: dockerLoading, error: dockerError } = useDocker( + connectionSource === 'local' ? 'local' : undefined, + connectionSource !== 'local' && connectionSource !== 'custom' ? connectionSource : undefined + ) + const [selectedDomain, setSelectedDomain] = useState('') const [selectedContainerId, setSelectedContainerId] = useState('') diff --git a/frontend/src/hooks/useDocker.ts b/frontend/src/hooks/useDocker.ts index b4f9d386..6416b499 100644 --- a/frontend/src/hooks/useDocker.ts +++ b/frontend/src/hooks/useDocker.ts @@ -1,16 +1,16 @@ import { useQuery } from '@tanstack/react-query' import { dockerApi } from '../api/docker' -export function useDocker(host?: string | null) { +export function useDocker(host?: string | null, serverId?: string | null) { const { data: containers = [], isLoading, error, refetch, } = useQuery({ - queryKey: ['docker-containers', host], - queryFn: () => dockerApi.listContainers(host || undefined), - enabled: host !== null, // Disable if host is explicitly null + queryKey: ['docker-containers', host, serverId], + queryFn: () => dockerApi.listContainers(host || undefined, serverId || undefined), + enabled: host !== null || serverId !== null, // Disable if both are explicitly null/undefined retry: 1, // Don't retry too much if docker is not available }) diff --git a/frontend/src/pages/Uptime.tsx b/frontend/src/pages/Uptime.tsx index dae92749..3afea85e 100644 --- a/frontend/src/pages/Uptime.tsx +++ b/frontend/src/pages/Uptime.tsx @@ -188,6 +188,10 @@ const Uptime: React.FC = () => { ); }, [monitors]); + const proxyHostMonitors = useMemo(() => sortedMonitors.filter(m => m.proxy_host_id), [sortedMonitors]); + const remoteServerMonitors = useMemo(() => sortedMonitors.filter(m => m.remote_server_id), [sortedMonitors]); + const otherMonitors = useMemo(() => sortedMonitors.filter(m => !m.proxy_host_id && !m.remote_server_id), [sortedMonitors]); + if (isLoading) { return
Loading monitors...
; } @@ -204,16 +208,46 @@ const Uptime: React.FC = () => { -
- {sortedMonitors.map((monitor) => ( - - ))} - {sortedMonitors.length === 0 && ( -
- No monitors found. Add a Proxy Host to start monitoring. -
- )} -
+ {sortedMonitors.length === 0 ? ( +
+ No monitors found. Add a Proxy Host or Remote Server to start monitoring. +
+ ) : ( + <> + {proxyHostMonitors.length > 0 && ( +
+

Proxy Hosts

+
+ {proxyHostMonitors.map((monitor) => ( + + ))} +
+
+ )} + + {remoteServerMonitors.length > 0 && ( +
+

Remote Servers

+
+ {remoteServerMonitors.map((monitor) => ( + + ))} +
+
+ )} + + {otherMonitors.length > 0 && ( +
+

Other Monitors

+
+ {otherMonitors.map((monitor) => ( + + ))} +
+
+ )} + + )} {editingMonitor && ( setEditingMonitor(null)} />