detached 58ca3d4

feat: Add image pruning functionality and enhance image management UI

- Introduced a new endpoint for purging unused Docker images via the `/image-manager/prune` route.
- Implemented the `ImageManagerPrune` handler to execute the pruning operation and return the number of images purged.
- Updated the image management UI to include a "Purge Unused" button, allowing users to easily remove dangling images.
- Refactored template components to utilize the new URL generation method for improved maintainability.
- Enhanced logging for image management operations to provide better traceability.
commit
58ca3d4f16c24a3e6bd1cec0a8e611ad35421334
author
bamen · · 2025-03-30 12:46 UTC
parent
5eadbfb
signature
unsigned
20 files · +347 -336
M go.mod +1 -1
@@ -249,7 +249,7 @@ require (
249249 github.com/Microsoft/go-winio v0.6.2 // indirect
250250 github.com/andybalholm/cascadia v1.3.3 // indirect
251251 github.com/aymerick/douceur v0.2.0 // indirect
252- github.com/docker/go-units v0.5.0 // indirect
252+ github.com/docker/go-units v0.5.0
253253 github.com/gogo/protobuf v1.3.2 // indirect
254254 github.com/google/uuid v1.6.0 // direct
255255 github.com/gorilla/context v1.1.2 // indirect
@@ -249,7 +249,7 @@ require (
249 github.com/Microsoft/go-winio v0.6.2 // indirect249 github.com/Microsoft/go-winio v0.6.2 // indirect
250 github.com/andybalholm/cascadia v1.3.3 // indirect250 github.com/andybalholm/cascadia v1.3.3 // indirect
251 github.com/aymerick/douceur v0.2.0 // indirect251 github.com/aymerick/douceur v0.2.0 // indirect
252- github.com/docker/go-units v0.5.0 // indirect252+ github.com/docker/go-units v0.5.0
253 github.com/gogo/protobuf v1.3.2 // indirect253 github.com/gogo/protobuf v1.3.2 // indirect
254 github.com/google/uuid v1.6.0 // direct254 github.com/google/uuid v1.6.0 // direct
255 github.com/gorilla/context v1.1.2 // indirect255 github.com/gorilla/context v1.1.2 // indirect
M internal/httpserve/handlers/container_management.go +9 -0
@@ -177,6 +177,15 @@ func ImageManagerDelete(c echo.Context, a *server.App) error {
177177 return c.String(http.StatusOK, ActionSuccess(a))
178178 }
179179
180+// ImageManagerPrune handles the /image-manager/prune route
181+func ImageManagerPrune(c echo.Context, a *server.App) error {
182+ numPurged, err := docker.PruneImages()
183+ if err != nil {
184+ return sendError(c, err)
185+ }
186+ return c.String(http.StatusOK, fmt.Sprintf("Successfully purged %d images", numPurged))
187+}
188+
180189 // ContainerManagerComponent handles the /container-manager route
181190 func ContainerManagerComponent(c echo.Context, a *server.App) error {
182191 logger.Debug("ContainerManagerComponent called", "url", c.Request().URL.String())
@@ -177,6 +177,15 @@ func ImageManagerDelete(c echo.Context, a *server.App) error {
177 return c.String(http.StatusOK, ActionSuccess(a))177 return c.String(http.StatusOK, ActionSuccess(a))
178 }178 }
179 179
180+// ImageManagerPrune handles the /image-manager/prune route
181+func ImageManagerPrune(c echo.Context, a *server.App) error {
182+ numPurged, err := docker.PruneImages()
183+ if err != nil {
184+ return sendError(c, err)
185+ }
186+ return c.String(http.StatusOK, fmt.Sprintf("Successfully purged %d images", numPurged))
187+}
188+
180 // ContainerManagerComponent handles the /container-manager route189 // ContainerManagerComponent handles the /container-manager route
181 func ContainerManagerComponent(c echo.Context, a *server.App) error {190 func ContainerManagerComponent(c echo.Context, a *server.App) error {
182 logger.Debug("ContainerManagerComponent called", "url", c.Request().URL.String())191 logger.Debug("ContainerManagerComponent called", "url", c.Request().URL.String())
M internal/httpserve/router.go +4 -0
@@ -221,6 +221,10 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) {
221221 htmxGroup.DELETE("/image-manager/delete/:ID", func(c echo.Context) error {
222222 return handlers.ImageManagerDelete(c, a)
223223 })
224+ // Purge unused images
225+ htmxGroup.POST("/image-manager/prune", func(c echo.Context) error {
226+ return handlers.ImageManagerPrune(c, a)
227+ })
224228
225229 // List all containers
226230 htmxGroup.GET("/container-manager", func(c echo.Context) error {
@@ -221,6 +221,10 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) {
221 htmxGroup.DELETE("/image-manager/delete/:ID", func(c echo.Context) error {221 htmxGroup.DELETE("/image-manager/delete/:ID", func(c echo.Context) error {
222 return handlers.ImageManagerDelete(c, a)222 return handlers.ImageManagerDelete(c, a)
223 })223 })
224+ // Purge unused images
225+ htmxGroup.POST("/image-manager/prune", func(c echo.Context) error {
226+ return handlers.ImageManagerPrune(c, a)
227+ })
224 228
225 // List all containers229 // List all containers
226 htmxGroup.GET("/container-manager", func(c echo.Context) error {230 htmxGroup.GET("/container-manager", func(c echo.Context) error {
M internal/proxy/acme.go +0 -1
@@ -27,7 +27,6 @@ type CertManagerConfig struct {
2727 Email string
2828 Mode string
2929 SkipCertificates bool
30- BehindTLSProxy bool
3130 AdminDomain string
3231 RootDomain string
3332 HttpPort string
@@ -27,7 +27,6 @@ type CertManagerConfig struct {
27 Email string27 Email string
28 Mode string28 Mode string
29 SkipCertificates bool29 SkipCertificates bool
30- BehindTLSProxy bool
31 AdminDomain string30 AdminDomain string
32 RootDomain string31 RootDomain string
33 HttpPort string32 HttpPort string
M internal/proxy/core.go +19 -55
@@ -4,8 +4,6 @@ import (
44 "context"
55 "fmt"
66 "net"
7- "os"
8- "strings"
97 "sync"
108 "time"
119
@@ -162,37 +160,38 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
162160 certDir = app.GetConfig().General.StorageDir + "/certs"
163161 }
164162
165- // Initial upstream proxy detection (before creating CertManager)
166- initialBehindTLSProxy := detectInitialUpstreamProxy(&config)
167-
168163 // Create Certificate Manager Config
169164 certManagerConfig := CertManagerConfig{
170165 CertDir: certDir,
171166 Email: config.Email,
172167 Mode: config.LetsEncryptMode, // staging or production
173168 SkipCertificates: config.SkipCertificates,
174- BehindTLSProxy: initialBehindTLSProxy, // Pass initial detection result
175169 AdminDomain: app.GetConfig().Http.FullDomain(),
176170 RootDomain: app.GetConfig().Http.Domain,
177171 HttpPort: config.DefaultHttpChallengePort,
178172 // RouteValidator: p.isHostInRoutes, // TODO: Pass a function if needed by hostPolicy
179173 }
180174
181- // Create the Certificate Manager
182- certMgr, err := NewCertificateManager(certManagerConfig, app, p.Queries)
183- if err != nil {
184- logger.Error("Failed to initialize Certificate Manager", "error", err)
185- // Return an error to prevent starting a broken proxy
186- return nil, fmt.Errorf("failed to initialize certificate manager: %w", err)
187- } else {
188- logger.Debug("Certificate manager successfully initialized")
189- p.certificateManager = certMgr // Store the new manager
190- }
175+ // Only initialize Certificate Manager if SkipCertificates is false
176+ if !config.SkipCertificates {
177+ // Create the Certificate Manager
178+ certMgr, err := NewCertificateManager(certManagerConfig, app, p.Queries)
179+ if err != nil {
180+ logger.Error("Failed to initialize Certificate Manager", "error", err)
181+ // Return an error to prevent starting a broken proxy
182+ return nil, fmt.Errorf("failed to initialize certificate manager: %w", err)
183+ } else {
184+ logger.Debug("Certificate manager successfully initialized")
185+ p.certificateManager = certMgr // Store the new manager
186+ }
191187
192- // Ensure admin domain has proper ACME configuration
193- if err := p.ensureAdminDomainConfig(); err != nil {
194- logger.Error("Failed to ensure admin domain configuration", "error", err)
195- return nil, fmt.Errorf("failed to ensure admin domain configuration: %w", err)
188+ // Ensure admin domain has proper ACME configuration only if manager was created
189+ if err := p.ensureAdminDomainConfig(); err != nil {
190+ logger.Error("Failed to ensure admin domain configuration", "error", err)
191+ return nil, fmt.Errorf("failed to ensure admin domain configuration: %w", err)
192+ }
193+ } else {
194+ logger.Info("Skipping certificate manager initialization because SkipCertificates is true")
196195 }
197196
198197 // --- End Certificate Manager Setup ---
@@ -217,41 +216,6 @@ func (p *Proxy) Close() {
217216 logger.Info("Proxy resources cleaned up")
218217 }
219218
220-// detectInitialUpstreamProxy checks environment variables for signs of an upstream proxy
221-// This is used during initialization before the CertificateManager is created.
222-func detectInitialUpstreamProxy(config *common.ReverseProxyConfig) bool {
223- // Check if detection is explicitly disabled
224- if !config.DetectUpstreamProxy {
225- logger.Debug("Upstream proxy detection disabled by config")
226- return false
227- }
228-
229- // Check for common environment variables set by upstream proxies
230- if os.Getenv("HTTPS") == "on" ||
231- os.Getenv("HTTP_X_FORWARDED_PROTO") == "https" ||
232- os.Getenv("HTTP_X_FORWARDED_SSL") == "on" {
233- logger.Info("Initial upstream TLS-terminating proxy detected via environment variables",
234- "detection_method", "environment_variables",
235- "https", os.Getenv("HTTPS"),
236- "x_forwarded_proto", os.Getenv("HTTP_X_FORWARDED_PROTO"),
237- "x_forwarded_ssl", os.Getenv("HTTP_X_FORWARDED_SSL"))
238- return true
239- }
240-
241- // Check Cloudflare headers
242- if cfVisitor := os.Getenv("HTTP_CF_VISITOR"); cfVisitor != "" {
243- if strings.Contains(cfVisitor, "\"scheme\":\"https\"") {
244- logger.Info("Initial Cloudflare proxy detected via environment variables",
245- "detection_method", "cloudflare_headers",
246- "cf_visitor", cfVisitor)
247- return true
248- }
249- }
250-
251- logger.Debug("No initial upstream proxy detected via environment variables")
252- return false
253-}
254-
255219 // requestDomainCertificate requests a certificate for a domain
256220 func (p *Proxy) requestDomainCertificate(domainName string) error {
257221 logger.Debug("Requesting certificate for domain", "domain", domainName)
@@ -4,8 +4,6 @@ import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6 "net"6 "net"
7- "os"
8- "strings"
9 "sync"7 "sync"
10 "time"8 "time"
11 9
@@ -162,37 +160,38 @@ func NewProxy(app interfaces.AppInterface) (*Proxy, error) {
162 certDir = app.GetConfig().General.StorageDir + "/certs"160 certDir = app.GetConfig().General.StorageDir + "/certs"
163 }161 }
164 162
165- // Initial upstream proxy detection (before creating CertManager)
166- initialBehindTLSProxy := detectInitialUpstreamProxy(&config)
167-
168 // Create Certificate Manager Config163 // Create Certificate Manager Config
169 certManagerConfig := CertManagerConfig{164 certManagerConfig := CertManagerConfig{
170 CertDir: certDir,165 CertDir: certDir,
171 Email: config.Email,166 Email: config.Email,
172 Mode: config.LetsEncryptMode, // staging or production167 Mode: config.LetsEncryptMode, // staging or production
173 SkipCertificates: config.SkipCertificates,168 SkipCertificates: config.SkipCertificates,
174- BehindTLSProxy: initialBehindTLSProxy, // Pass initial detection result
175 AdminDomain: app.GetConfig().Http.FullDomain(),169 AdminDomain: app.GetConfig().Http.FullDomain(),
176 RootDomain: app.GetConfig().Http.Domain,170 RootDomain: app.GetConfig().Http.Domain,
177 HttpPort: config.DefaultHttpChallengePort,171 HttpPort: config.DefaultHttpChallengePort,
178 // RouteValidator: p.isHostInRoutes, // TODO: Pass a function if needed by hostPolicy172 // RouteValidator: p.isHostInRoutes, // TODO: Pass a function if needed by hostPolicy
179 }173 }
180 174
181- // Create the Certificate Manager175+ // Only initialize Certificate Manager if SkipCertificates is false
182- certMgr, err := NewCertificateManager(certManagerConfig, app, p.Queries)176+ if !config.SkipCertificates {
183- if err != nil {177+ // Create the Certificate Manager
184- logger.Error("Failed to initialize Certificate Manager", "error", err)178+ certMgr, err := NewCertificateManager(certManagerConfig, app, p.Queries)
185- // Return an error to prevent starting a broken proxy179+ if err != nil {
186- return nil, fmt.Errorf("failed to initialize certificate manager: %w", err)180+ logger.Error("Failed to initialize Certificate Manager", "error", err)
187- } else {181+ // Return an error to prevent starting a broken proxy
188- logger.Debug("Certificate manager successfully initialized")182+ return nil, fmt.Errorf("failed to initialize certificate manager: %w", err)
189- p.certificateManager = certMgr // Store the new manager183+ } else {
190- }184+ logger.Debug("Certificate manager successfully initialized")
185+ p.certificateManager = certMgr // Store the new manager
186+ }
191 187
192- // Ensure admin domain has proper ACME configuration188+ // Ensure admin domain has proper ACME configuration only if manager was created
193- if err := p.ensureAdminDomainConfig(); err != nil {189+ if err := p.ensureAdminDomainConfig(); err != nil {
194- logger.Error("Failed to ensure admin domain configuration", "error", err)190+ logger.Error("Failed to ensure admin domain configuration", "error", err)
195- return nil, fmt.Errorf("failed to ensure admin domain configuration: %w", err)191+ return nil, fmt.Errorf("failed to ensure admin domain configuration: %w", err)
192+ }
193+ } else {
194+ logger.Info("Skipping certificate manager initialization because SkipCertificates is true")
196 }195 }
197 196
198 // --- End Certificate Manager Setup ---197 // --- End Certificate Manager Setup ---
@@ -217,41 +216,6 @@ func (p *Proxy) Close() {
217 logger.Info("Proxy resources cleaned up")216 logger.Info("Proxy resources cleaned up")
218 }217 }
219 218
220-// detectInitialUpstreamProxy checks environment variables for signs of an upstream proxy
221-// This is used during initialization before the CertificateManager is created.
222-func detectInitialUpstreamProxy(config *common.ReverseProxyConfig) bool {
223- // Check if detection is explicitly disabled
224- if !config.DetectUpstreamProxy {
225- logger.Debug("Upstream proxy detection disabled by config")
226- return false
227- }
228-
229- // Check for common environment variables set by upstream proxies
230- if os.Getenv("HTTPS") == "on" ||
231- os.Getenv("HTTP_X_FORWARDED_PROTO") == "https" ||
232- os.Getenv("HTTP_X_FORWARDED_SSL") == "on" {
233- logger.Info("Initial upstream TLS-terminating proxy detected via environment variables",
234- "detection_method", "environment_variables",
235- "https", os.Getenv("HTTPS"),
236- "x_forwarded_proto", os.Getenv("HTTP_X_FORWARDED_PROTO"),
237- "x_forwarded_ssl", os.Getenv("HTTP_X_FORWARDED_SSL"))
238- return true
239- }
240-
241- // Check Cloudflare headers
242- if cfVisitor := os.Getenv("HTTP_CF_VISITOR"); cfVisitor != "" {
243- if strings.Contains(cfVisitor, "\"scheme\":\"https\"") {
244- logger.Info("Initial Cloudflare proxy detected via environment variables",
245- "detection_method", "cloudflare_headers",
246- "cf_visitor", cfVisitor)
247- return true
248- }
249- }
250-
251- logger.Debug("No initial upstream proxy detected via environment variables")
252- return false
253-}
254-
255 // requestDomainCertificate requests a certificate for a domain219 // requestDomainCertificate requests a certificate for a domain
256 func (p *Proxy) requestDomainCertificate(domainName string) error {220 func (p *Proxy) requestDomainCertificate(domainName string) error {
257 logger.Debug("Requesting certificate for domain", "domain", domainName)221 logger.Debug("Requesting certificate for domain", "domain", domainName)
M internal/proxy/middleware.go +21 -0
@@ -1,7 +1,9 @@
11 package proxy
22
33 import (
4+ "fmt"
45 "net/http"
6+ "strings"
57 "time"
68
79 "github.com/bnema/gordon/pkg/logger"
@@ -28,6 +30,9 @@ func (p *Proxy) setupMiddleware() {
2830 // Add UUID generator middleware for both HTTP and HTTPS servers
2931 p.httpsServer.Use(p.createRequestIDMiddleware())
3032
33+ // Add middleware to log incoming headers (for debugging Cloudflare detection)
34+ p.httpsServer.Use(logHeadersMiddleware())
35+
3136 // Check if rate limiting is disabled via configuration
3237 if p.config.EnableRateLimit {
3338 // Create a new Starskey rate limiter store for HTTPS server
@@ -215,3 +220,19 @@ func (p *Proxy) createRequestIDMiddleware() echo.MiddlewareFunc {
215220 }
216221 }
217222 }
223+
224+// logHeadersMiddleware creates a middleware function to log request headers
225+func logHeadersMiddleware() echo.MiddlewareFunc {
226+ return func(next echo.HandlerFunc) echo.HandlerFunc {
227+ return func(c echo.Context) error {
228+ // Log all headers at DEBUG level
229+ headerStrings := []string{}
230+ for k, v := range c.Request().Header {
231+ headerStrings = append(headerStrings, fmt.Sprintf("%s: %s", k, strings.Join(v, ",")))
232+ }
233+ logger.Debug("Incoming Request Headers", "request_id", c.Get(RequestIDKey), "headers", strings.Join(headerStrings, " | "))
234+
235+ return next(c)
236+ }
237+ }
238+}
@@ -1,7 +1,9 @@
1 package proxy1 package proxy
2 2
3 import (3 import (
4+ "fmt"
4 "net/http"5 "net/http"
6+ "strings"
5 "time"7 "time"
6 8
7 "github.com/bnema/gordon/pkg/logger"9 "github.com/bnema/gordon/pkg/logger"
@@ -28,6 +30,9 @@ func (p *Proxy) setupMiddleware() {
28 // Add UUID generator middleware for both HTTP and HTTPS servers30 // Add UUID generator middleware for both HTTP and HTTPS servers
29 p.httpsServer.Use(p.createRequestIDMiddleware())31 p.httpsServer.Use(p.createRequestIDMiddleware())
30 32
33+ // Add middleware to log incoming headers (for debugging Cloudflare detection)
34+ p.httpsServer.Use(logHeadersMiddleware())
35+
31 // Check if rate limiting is disabled via configuration36 // Check if rate limiting is disabled via configuration
32 if p.config.EnableRateLimit {37 if p.config.EnableRateLimit {
33 // Create a new Starskey rate limiter store for HTTPS server38 // Create a new Starskey rate limiter store for HTTPS server
@@ -215,3 +220,19 @@ func (p *Proxy) createRequestIDMiddleware() echo.MiddlewareFunc {
215 }220 }
216 }221 }
217 }222 }
223+
224+// logHeadersMiddleware creates a middleware function to log request headers
225+func logHeadersMiddleware() echo.MiddlewareFunc {
226+ return func(next echo.HandlerFunc) echo.HandlerFunc {
227+ return func(c echo.Context) error {
228+ // Log all headers at DEBUG level
229+ headerStrings := []string{}
230+ for k, v := range c.Request().Header {
231+ headerStrings = append(headerStrings, fmt.Sprintf("%s: %s", k, strings.Join(v, ",")))
232+ }
233+ logger.Debug("Incoming Request Headers", "request_id", c.Get(RequestIDKey), "headers", strings.Join(headerStrings, " | "))
234+
235+ return next(c)
236+ }
237+ }
238+}
M internal/proxy/routes.go +0 -38
@@ -1971,44 +1971,6 @@ func (p *Proxy) doProxyRequest(c echo.Context, route *ProxyRouteInfo) error {
19711971 c.Response().Write([]byte("Bad Gateway: Container is unreachable"))
19721972 }
19731973
1974- // For admin domain, ensure the connection succeeds
1975- if isAdminDomain {
1976- // Do a pre-check to make sure the admin service is reachable
1977- testURL := fmt.Sprintf("http://%s:%s/admin/ping", containerIP, containerPort)
1978- client := &http.Client{
1979- Timeout: 2 * time.Second,
1980- Transport: &http.Transport{
1981- DialContext: (&net.Dialer{
1982- Timeout: 1 * time.Second,
1983- }).DialContext,
1984- },
1985- }
1986-
1987- logger.Debug("Testing admin connection before proxying",
1988- "test_url", testURL)
1989-
1990- _, err := client.Get(testURL)
1991- if err != nil {
1992- logger.Warn("Admin service pre-check failed, trying alternative connection methods",
1993- "error", err)
1994-
1995- // Try alternative IPs for the admin domain if the container IP isn't working
1996- altIPs := []string{"localhost", "127.0.0.1", p.app.GetConfig().Http.SubDomain}
1997-
1998- for _, altIP := range altIPs {
1999- testURL = fmt.Sprintf("http://%s:%s/admin/ping", altIP, containerPort)
2000- _, err := client.Get(testURL)
2001- if err == nil {
2002- logger.Info("Found working admin connection with alternative IP",
2003- "ip", altIP)
2004- containerIP = altIP
2005- targetURL.Host = fmt.Sprintf("%s:%s", containerIP, containerPort)
2006- break
2007- }
2008- }
2009- }
2010- }
2011-
20121974 // Serve the request using the reverse proxy
20131975 logger.Debug("Serving proxy request",
20141976 "target", targetURL.String())
@@ -1971,44 +1971,6 @@ func (p *Proxy) doProxyRequest(c echo.Context, route *ProxyRouteInfo) error {
1971 c.Response().Write([]byte("Bad Gateway: Container is unreachable"))1971 c.Response().Write([]byte("Bad Gateway: Container is unreachable"))
1972 }1972 }
1973 1973
1974- // For admin domain, ensure the connection succeeds
1975- if isAdminDomain {
1976- // Do a pre-check to make sure the admin service is reachable
1977- testURL := fmt.Sprintf("http://%s:%s/admin/ping", containerIP, containerPort)
1978- client := &http.Client{
1979- Timeout: 2 * time.Second,
1980- Transport: &http.Transport{
1981- DialContext: (&net.Dialer{
1982- Timeout: 1 * time.Second,
1983- }).DialContext,
1984- },
1985- }
1986-
1987- logger.Debug("Testing admin connection before proxying",
1988- "test_url", testURL)
1989-
1990- _, err := client.Get(testURL)
1991- if err != nil {
1992- logger.Warn("Admin service pre-check failed, trying alternative connection methods",
1993- "error", err)
1994-
1995- // Try alternative IPs for the admin domain if the container IP isn't working
1996- altIPs := []string{"localhost", "127.0.0.1", p.app.GetConfig().Http.SubDomain}
1997-
1998- for _, altIP := range altIPs {
1999- testURL = fmt.Sprintf("http://%s:%s/admin/ping", altIP, containerPort)
2000- _, err := client.Get(testURL)
2001- if err == nil {
2002- logger.Info("Found working admin connection with alternative IP",
2003- "ip", altIP)
2004- containerIP = altIP
2005- targetURL.Host = fmt.Sprintf("%s:%s", containerIP, containerPort)
2006- break
2007- }
2008- }
2009- }
2010- }
2011-
2012 // Serve the request using the reverse proxy1974 // Serve the request using the reverse proxy
2013 logger.Debug("Serving proxy request",1975 logger.Debug("Serving proxy request",
2014 "target", targetURL.String())1976 "target", targetURL.String())
M internal/proxy/server.go +121 -102
@@ -3,6 +3,7 @@ package proxy
33 import (
44 "context"
55 "crypto/tls"
6+ "errors"
67 "fmt"
78 "net/http"
89 "strings"
@@ -209,12 +210,13 @@ func (p *Proxy) Stop() error {
209210 return nil
210211 }
211212
212-// testAdminConnection attempts to find a working connection to the Gordon admin
213+// testAdminConnection attempts to find a working connection to the Gordon admin ping endpoint
213214 func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
214- adminPath := p.app.GetConfig().Admin.Path
215- url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, adminPath)
215+ adminPath := p.app.GetConfig().Admin.Path // Get the base admin path
216+ pingPath := adminPath + "/ping" // Construct the full ping path
217+ url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, pingPath)
216218
217- logger.Debug("Testing admin connection", "url", url)
219+ logger.Debug("Testing admin ping connection", "url", url) // Update log message
218220
219221 // Try connecting using the provided default host
220222 client := &http.Client{
@@ -230,11 +232,18 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
230232 resp, err := client.Get(url)
231233 if err == nil {
232234 resp.Body.Close()
233- logger.Info("Admin connection successful", "host", defaultHost)
234- return defaultHost
235+ // Check if the response status is OK
236+ if resp.StatusCode == http.StatusOK {
237+ logger.Info("Admin ping connection successful", "host", defaultHost) // Update log message
238+ return defaultHost
239+ } else {
240+ logger.Warn("Admin ping connection returned non-OK status", "host", defaultHost, "status_code", resp.StatusCode)
241+ }
242+ } else {
243+ logger.Warn("Admin ping connection failed", "host", defaultHost, "error", err)
235244 }
236245
237- // If connection failed, try other common hostnames
246+ // If connection failed or status not OK, try other common hostnames
238247 hosts := []string{
239248 "localhost",
240249 "127.0.0.1",
@@ -247,18 +256,24 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
247256 continue // Already tried this one
248257 }
249258
250- url = fmt.Sprintf("http://%s:%s%s", host, port, adminPath)
251- logger.Debug("Trying alternative host", "url", url)
259+ url = fmt.Sprintf("http://%s:%s%s", host, port, pingPath) // Use full pingPath here too
260+ logger.Debug("Trying alternative host for admin ping", "url", url) // Update log message
252261
253262 resp, err := client.Get(url)
254263 if err == nil {
255264 resp.Body.Close()
256- logger.Info("Found working admin connection", "host", host)
257- return host
265+ if resp.StatusCode == http.StatusOK {
266+ logger.Info("Found working admin ping connection", "host", host) // Update log message
267+ return host
268+ } else {
269+ logger.Warn("Admin ping connection on alternative host returned non-OK status", "host", host, "status_code", resp.StatusCode)
270+ }
271+ } else {
272+ logger.Warn("Admin ping connection failed on alternative host", "host", host, "error", err)
258273 }
259274 }
260275
261- logger.Warn("Could not establish admin connection on any tested host", "fallback", defaultHost)
276+ logger.Warn("Could not establish admin ping connection on any tested host", "fallback", defaultHost) // Update log message
262277 return defaultHost
263278 }
264279
@@ -473,105 +488,109 @@ func (p *Proxy) checkExternalPortAccess() {
473488
474489 // StartHTTPSServer starts the HTTPS server
475490 func (p *Proxy) StartHTTPSServer() error {
476- logger.Info("Starting HTTPS server",
477- "port", p.config.Port,
478- "mode", p.config.LetsEncryptMode)
479-
480- // Verify the certificate manager is initialized (it's done in NewProxy now)
481- if p.certificateManager == nil {
482- // This should ideally not happen if NewProxy succeeded
483- logger.Error("Certificate manager is nil in StartHTTPSServer, cannot start HTTPS")
484- return fmt.Errorf("certificate manager not initialized")
485- }
486- logger.Info("Using new certificate management system")
487-
488- // Setup TLS configuration
489- tlsConfig := &tls.Config{
490- // Use the helper function that gets the GetCertificate func from the manager
491- GetCertificate: p.getTLSCertificateFunc(),
492- // Ensure we use TLS 1.2 or higher
493- MinVersion: tls.VersionTLS12,
494- // Optimize cipher suites for security and performance
495- CipherSuites: []uint16{
496- tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
497- tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
498- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
499- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
500- tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
501- tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
502- },
503- // Use server's preferred ciphersuite
504- PreferServerCipherSuites: true,
505- // Enable HTTP/2
506- NextProtos: []string{"h2", "http/1.1"},
507- // Add server name to use when client doesn't send SNI
508- ServerName: p.app.GetConfig().Http.FullDomain(),
509- }
510-
511- // Removed setup for ACME challenge handler on the persistent HTTP server,
512- // as it's no longer running. Lego's temporary server will handle challenges.
513- // p.httpServer.Any("/.well-known/acme-challenge/*", echo.WrapHandler(p.getACMEHTTPHandler()))
514-
515- // Create a custom HTTPS server with proper TLS config
516- httpsServer := &http.Server{
517- Addr: fmt.Sprintf(":%s", p.config.Port),
518- Handler: p.httpsServer,
519- TLSConfig: tlsConfig,
520- // Add increased timeouts to prevent hanging connections
521- ReadTimeout: 60 * time.Second,
522- WriteTimeout: 60 * time.Second,
523- IdleTimeout: 120 * time.Second,
524- ReadHeaderTimeout: 30 * time.Second, // Add header timeout
525- }
491+ // Setup main Echo instance for routing (used by both HTTP/HTTPS handlers)
492+ e := p.httpsServer // Use the existing Echo instance
493+
494+ // If SkipCertificates is true, start a plain HTTP listener instead of HTTPS
495+ if p.config.SkipCertificates {
496+ logger.Info("Starting plain HTTP listener because SkipCertificates is true",
497+ "port", p.config.DefaultHttpChallengePort)
498+
499+ // Create a standard http.Server for the plain HTTP listener
500+ httpServer := &http.Server{
501+ Addr: fmt.Sprintf(":%s", p.config.DefaultHttpChallengePort), // Add colon prefix
502+ Handler: e, // Use the main Echo instance
503+ ReadTimeout: 5 * time.Second,
504+ WriteTimeout: 10 * time.Second,
505+ IdleTimeout: 120 * time.Second,
506+ }
526507
527- // Store the HTTPS server for later use in Stop()
528- p.httpsServer.Server = httpsServer
508+ // Start plain HTTP listener using http.Server's ListenAndServe
509+ go func() {
510+ logger.Info("Plain HTTP listener starting", "address", httpServer.Addr)
511+ if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
512+ // Log specific errors if possible
513+ if strings.Contains(err.Error(), "bind: address already in use") {
514+ logger.Error("Plain HTTP port is already in use", "port", p.config.DefaultHttpChallengePort, "error", err)
515+ } else if strings.Contains(err.Error(), "permission denied") {
516+ logger.Error("Permission denied binding to plain HTTP port", "port", p.config.DefaultHttpChallengePort, "error", err)
517+ } else {
518+ logger.Error("Plain HTTP listener failed", "error", err, "address", httpServer.Addr)
519+ }
520+ }
521+ }()
522+ p.serverStarted = true // Mark server as started
529523
530- // // Create a custom HTTP server
531- // httpServer := &http.Server{
532- // Addr: fmt.Sprintf(":%s", p.config.HttpPort),
533- // Handler: p.httpServer,
534- // ReadTimeout: 30 * time.Second,
535- // WriteTimeout: 30 * time.Second,
536- // IdleTimeout: 120 * time.Second,
537- // }
524+ } else {
525+ // --- Start HTTPS Server (only if SkipCertificates is false) ---
526+ logger.Info("Starting HTTPS server",
527+ "port", p.config.Port,
528+ "mode", p.config.LetsEncryptMode)
538529
539- p.serverStarted = true
530+ // Verify certificate manager is initialized *only* if we need it for HTTPS
531+ if p.certificateManager == nil {
532+ logger.Error("Certificate manager is nil, cannot start HTTPS server")
533+ return fmt.Errorf("certificate manager not initialized for HTTPS")
534+ }
535+ logger.Info("Using certificate management system for HTTPS")
536+
537+ // Setup TLS configuration
538+ tlsConfig := &tls.Config{
539+ GetCertificate: p.getTLSCertificateFunc(),
540+ MinVersion: tls.VersionTLS12,
541+ CipherSuites: []uint16{
542+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
543+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
544+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
545+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
546+ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
547+ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
548+ },
549+ PreferServerCipherSuites: true,
550+ NextProtos: []string{"h2", "http/1.1"},
551+ ServerName: p.app.GetConfig().Http.FullDomain(),
552+ }
540553
541- // httpServer is no longer needed as ACME HTTP-01 uses its own temporary server
542- // httpServer := &http.Server{
543- // Addr: fmt.Sprintf(":%s", p.config.HttpPort),
544- // Handler: p.httpServer,
545- // ReadTimeout: 30 * time.Second,
546- // WriteTimeout: 30 * time.Second,
547- // IdleTimeout: 120 * time.Second,
548- // }
554+ // Create a custom HTTPS server with proper TLS config
555+ httpsServer := &http.Server{
556+ Addr: fmt.Sprintf(":%s", p.config.Port), // Use the configured HTTPS port (e.g., :443)
557+ Handler: e, // Use the main Echo instance
558+ TLSConfig: tlsConfig,
559+ ReadTimeout: 60 * time.Second,
560+ WriteTimeout: 60 * time.Second,
561+ IdleTimeout: 120 * time.Second,
562+ ReadHeaderTimeout: 30 * time.Second,
563+ }
549564
550- // Start HTTPS server with more error details
551- go func() {
552- logger.Info("Starting HTTPS server", "address", httpsServer.Addr)
553- // Using empty strings for cert and key files since we're using GetCertificate
554- if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
555- logger.Error("HTTPS server failed", "error", err, "address", httpsServer.Addr)
556-
557- // Print more diagnostic information
558- if strings.Contains(err.Error(), "bind: address already in use") {
559- logger.Error("Port 443 is already in use by another process. Check if another web server is running.",
560- "solution", "Stop other services using port 443 or configure Gordon to use a different HTTPS port")
561- } else if strings.Contains(err.Error(), "permission denied") {
562- logger.Error("Permission denied when binding to port 443. This often happens when running without root privileges.",
563- "solution", "Use setcap to allow binding to privileged ports or run in a container with proper port mapping")
564- } else if strings.Contains(err.Error(), "certificate") {
565- logger.Error("Certificate-related error when starting HTTPS server",
566- "solution", "Check certificate files and permissions in the certificate directory")
565+ // Store the HTTPS server for later use in Stop()
566+ p.httpsServer.Server = httpsServer // Store the http.Server within Echo
567+
568+ // Start HTTPS server in a goroutine
569+ go func() {
570+ logger.Info("HTTPS server starting", "address", httpsServer.Addr)
571+ // Using empty strings for cert and key files since GetCertificate is used
572+ if err := httpsServer.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
573+ // Log specific errors if possible
574+ if strings.Contains(err.Error(), "bind: address already in use") {
575+ logger.Error("HTTPS port is already in use", "port", p.config.Port, "error", err)
576+ } else if strings.Contains(err.Error(), "permission denied") {
577+ logger.Error("Permission denied binding to HTTPS port", "port", p.config.Port, "error", err)
578+ } else if strings.Contains(err.Error(), "certificate") {
579+ logger.Error("Certificate-related error starting HTTPS server", "error", err)
580+ } else {
581+ logger.Error("HTTPS server failed", "error", err, "address", httpsServer.Addr)
582+ }
567583 }
568- }
569- }()
584+ }()
585+ p.serverStarted = true // Mark server as started
586+ }
570587
571- // Test admin connection after server starts
588+ // Test admin connection after attempting to start the appropriate server
572589 go p.TestAdminConnectionLater()
573590
574- return nil
591+ // Remove the old/duplicate HTTP listener logic that was here
592+
593+ return nil // Return nil indicating server start was initiated
575594 }
576595
577596 // getTLSCertificateFunc returns a function that gets certificates for TLS handshakes
@@ -3,6 +3,7 @@ package proxy
3 import (3 import (
4 "context"4 "context"
5 "crypto/tls"5 "crypto/tls"
6+ "errors"
6 "fmt"7 "fmt"
7 "net/http"8 "net/http"
8 "strings"9 "strings"
@@ -209,12 +210,13 @@ func (p *Proxy) Stop() error {
209 return nil210 return nil
210 }211 }
211 212
212-// testAdminConnection attempts to find a working connection to the Gordon admin213+// testAdminConnection attempts to find a working connection to the Gordon admin ping endpoint
213 func (p *Proxy) testAdminConnection(defaultHost string, port string) string {214 func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
214- adminPath := p.app.GetConfig().Admin.Path215+ adminPath := p.app.GetConfig().Admin.Path // Get the base admin path
215- url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, adminPath)216+ pingPath := adminPath + "/ping" // Construct the full ping path
217+ url := fmt.Sprintf("http://%s:%s%s", defaultHost, port, pingPath)
216 218
217- logger.Debug("Testing admin connection", "url", url)219+ logger.Debug("Testing admin ping connection", "url", url) // Update log message
218 220
219 // Try connecting using the provided default host221 // Try connecting using the provided default host
220 client := &http.Client{222 client := &http.Client{
@@ -230,11 +232,18 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
230 resp, err := client.Get(url)232 resp, err := client.Get(url)
231 if err == nil {233 if err == nil {
232 resp.Body.Close()234 resp.Body.Close()
233- logger.Info("Admin connection successful", "host", defaultHost)235+ // Check if the response status is OK
234- return defaultHost236+ if resp.StatusCode == http.StatusOK {
237+ logger.Info("Admin ping connection successful", "host", defaultHost) // Update log message
238+ return defaultHost
239+ } else {
240+ logger.Warn("Admin ping connection returned non-OK status", "host", defaultHost, "status_code", resp.StatusCode)
241+ }
242+ } else {
243+ logger.Warn("Admin ping connection failed", "host", defaultHost, "error", err)
235 }244 }
236 245
237- // If connection failed, try other common hostnames246+ // If connection failed or status not OK, try other common hostnames
238 hosts := []string{247 hosts := []string{
239 "localhost",248 "localhost",
240 "127.0.0.1",249 "127.0.0.1",
@@ -247,18 +256,24 @@ func (p *Proxy) testAdminConnection(defaultHost string, port string) string {
247 continue // Already tried this one256 continue // Already tried this one
248 }257 }
249 258
250- url = fmt.Sprintf("http://%s:%s%s", host, port, adminPath)259+ url = fmt.Sprintf("http://%s:%s%s", host, port, pingPath) // Use full pingPath here too
251- logger.Debug("Trying alternative host", "url", url)260+ logger.Debug("Trying alternative host for admin ping", "url", url) // Update log message
252 261
253 resp, err := client.Get(url)262 resp, err := client.Get(url)
254 if err == nil {263 if err == nil {
255 resp.Body.Close()264 resp.Body.Close()
256- logger.Info("Found working admin connection", "host", host)265+ if resp.StatusCode == http.StatusOK {
257- return host266+ logger.Info("Found working admin ping connection", "host", host) // Update log message
267+ return host
268+ } else {
269+ logger.Warn("Admin ping connection on alternative host returned non-OK status", "host", host, "status_code", resp.StatusCode)
270+ }
271+ } else {
272+ logger.Warn("Admin ping connection failed on alternative host", "host", host, "error", err)
258 }273 }
259 }274 }
260 275
261- logger.Warn("Could not establish admin connection on any tested host", "fallback", defaultHost)276+ logger.Warn("Could not establish admin ping connection on any tested host", "fallback", defaultHost) // Update log message
262 return defaultHost277 return defaultHost
263 }278 }
264 279
@@ -473,105 +488,109 @@ func (p *Proxy) checkExternalPortAccess() {
473 488
474 // StartHTTPSServer starts the HTTPS server489 // StartHTTPSServer starts the HTTPS server
475 func (p *Proxy) StartHTTPSServer() error {490 func (p *Proxy) StartHTTPSServer() error {
476- logger.Info("Starting HTTPS server",491+ // Setup main Echo instance for routing (used by both HTTP/HTTPS handlers)
477- "port", p.config.Port,492+ e := p.httpsServer // Use the existing Echo instance
478- "mode", p.config.LetsEncryptMode)493+
479-494+ // If SkipCertificates is true, start a plain HTTP listener instead of HTTPS
480- // Verify the certificate manager is initialized (it's done in NewProxy now)495+ if p.config.SkipCertificates {
481- if p.certificateManager == nil {496+ logger.Info("Starting plain HTTP listener because SkipCertificates is true",
482- // This should ideally not happen if NewProxy succeeded497+ "port", p.config.DefaultHttpChallengePort)
483- logger.Error("Certificate manager is nil in StartHTTPSServer, cannot start HTTPS")498+
484- return fmt.Errorf("certificate manager not initialized")499+ // Create a standard http.Server for the plain HTTP listener
485- }500+ httpServer := &http.Server{
486- logger.Info("Using new certificate management system")501+ Addr: fmt.Sprintf(":%s", p.config.DefaultHttpChallengePort), // Add colon prefix
487-502+ Handler: e, // Use the main Echo instance
488- // Setup TLS configuration503+ ReadTimeout: 5 * time.Second,
489- tlsConfig := &tls.Config{504+ WriteTimeout: 10 * time.Second,
490- // Use the helper function that gets the GetCertificate func from the manager505+ IdleTimeout: 120 * time.Second,
491- GetCertificate: p.getTLSCertificateFunc(),506+ }
492- // Ensure we use TLS 1.2 or higher
493- MinVersion: tls.VersionTLS12,
494- // Optimize cipher suites for security and performance
495- CipherSuites: []uint16{
496- tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
497- tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
498- tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
499- tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
500- tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
501- tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
502- },
503- // Use server's preferred ciphersuite
504- PreferServerCipherSuites: true,
505- // Enable HTTP/2
506- NextProtos: []string{"h2", "http/1.1"},
507- // Add server name to use when client doesn't send SNI
508- ServerName: p.app.GetConfig().Http.FullDomain(),
509- }
510-
511- // Removed setup for ACME challenge handler on the persistent HTTP server,
512- // as it's no longer running. Lego's temporary server will handle challenges.
513- // p.httpServer.Any("/.well-known/acme-challenge/*", echo.WrapHandler(p.getACMEHTTPHandler()))
514-
515- // Create a custom HTTPS server with proper TLS config
516- httpsServer := &http.Server{
517- Addr: fmt.Sprintf(":%s", p.config.Port),
518- Handler: p.httpsServer,
519- TLSConfig: tlsConfig,
520- // Add increased timeouts to prevent hanging connections
521- ReadTimeout: 60 * time.Second,
522- WriteTimeout: 60 * time.Second,
523- IdleTimeout: 120 * time.Second,
524- ReadHeaderTimeout: 30 * time.Second, // Add header timeout
525- }
526 507
527- // Store the HTTPS server for later use in Stop()508+ // Start plain HTTP listener using http.Server's ListenAndServe
528- p.httpsServer.Server = httpsServer509+ go func() {
510+ logger.Info("Plain HTTP listener starting", "address", httpServer.Addr)
511+ if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
512+ // Log specific errors if possible
513+ if strings.Contains(err.Error(), "bind: address already in use") {
514+ logger.Error("Plain HTTP port is already in use", "port", p.config.DefaultHttpChallengePort, "error", err)
515+ } else if strings.Contains(err.Error(), "permission denied") {
516+ logger.Error("Permission denied binding to plain HTTP port", "port", p.config.DefaultHttpChallengePort, "error", err)
517+ } else {
518+ logger.Error("Plain HTTP listener failed", "error", err, "address", httpServer.Addr)
519+ }
520+ }
521+ }()
522+ p.serverStarted = true // Mark server as started
529 523
530- // // Create a custom HTTP server524+ } else {
531- // httpServer := &http.Server{525+ // --- Start HTTPS Server (only if SkipCertificates is false) ---
532- // Addr: fmt.Sprintf(":%s", p.config.HttpPort),526+ logger.Info("Starting HTTPS server",
533- // Handler: p.httpServer,527+ "port", p.config.Port,
534- // ReadTimeout: 30 * time.Second,528+ "mode", p.config.LetsEncryptMode)
535- // WriteTimeout: 30 * time.Second,
536- // IdleTimeout: 120 * time.Second,
537- // }
538 529
539- p.serverStarted = true530+ // Verify certificate manager is initialized *only* if we need it for HTTPS
531+ if p.certificateManager == nil {
532+ logger.Error("Certificate manager is nil, cannot start HTTPS server")
533+ return fmt.Errorf("certificate manager not initialized for HTTPS")
534+ }
535+ logger.Info("Using certificate management system for HTTPS")
536+
537+ // Setup TLS configuration
538+ tlsConfig := &tls.Config{
539+ GetCertificate: p.getTLSCertificateFunc(),
540+ MinVersion: tls.VersionTLS12,
541+ CipherSuites: []uint16{
542+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
543+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
544+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
545+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
546+ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
547+ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
548+ },
549+ PreferServerCipherSuites: true,
550+ NextProtos: []string{"h2", "http/1.1"},
551+ ServerName: p.app.GetConfig().Http.FullDomain(),
552+ }
540 553
541- // httpServer is no longer needed as ACME HTTP-01 uses its own temporary server554+ // Create a custom HTTPS server with proper TLS config
542- // httpServer := &http.Server{555+ httpsServer := &http.Server{
543- // Addr: fmt.Sprintf(":%s", p.config.HttpPort),556+ Addr: fmt.Sprintf(":%s", p.config.Port), // Use the configured HTTPS port (e.g., :443)
544- // Handler: p.httpServer,557+ Handler: e, // Use the main Echo instance
545- // ReadTimeout: 30 * time.Second,558+ TLSConfig: tlsConfig,
546- // WriteTimeout: 30 * time.Second,559+ ReadTimeout: 60 * time.Second,
547- // IdleTimeout: 120 * time.Second,560+ WriteTimeout: 60 * time.Second,
548- // }561+ IdleTimeout: 120 * time.Second,
562+ ReadHeaderTimeout: 30 * time.Second,
563+ }
549 564
550- // Start HTTPS server with more error details565+ // Store the HTTPS server for later use in Stop()
551- go func() {566+ p.httpsServer.Server = httpsServer // Store the http.Server within Echo
552- logger.Info("Starting HTTPS server", "address", httpsServer.Addr)567+
553- // Using empty strings for cert and key files since we're using GetCertificate568+ // Start HTTPS server in a goroutine
554- if err := httpsServer.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {569+ go func() {
555- logger.Error("HTTPS server failed", "error", err, "address", httpsServer.Addr)570+ logger.Info("HTTPS server starting", "address", httpsServer.Addr)
556-571+ // Using empty strings for cert and key files since GetCertificate is used
557- // Print more diagnostic information572+ if err := httpsServer.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) {
558- if strings.Contains(err.Error(), "bind: address already in use") {573+ // Log specific errors if possible
559- logger.Error("Port 443 is already in use by another process. Check if another web server is running.",574+ if strings.Contains(err.Error(), "bind: address already in use") {
560- "solution", "Stop other services using port 443 or configure Gordon to use a different HTTPS port")575+ logger.Error("HTTPS port is already in use", "port", p.config.Port, "error", err)
561- } else if strings.Contains(err.Error(), "permission denied") {576+ } else if strings.Contains(err.Error(), "permission denied") {
562- logger.Error("Permission denied when binding to port 443. This often happens when running without root privileges.",577+ logger.Error("Permission denied binding to HTTPS port", "port", p.config.Port, "error", err)
563- "solution", "Use setcap to allow binding to privileged ports or run in a container with proper port mapping")578+ } else if strings.Contains(err.Error(), "certificate") {
564- } else if strings.Contains(err.Error(), "certificate") {579+ logger.Error("Certificate-related error starting HTTPS server", "error", err)
565- logger.Error("Certificate-related error when starting HTTPS server",580+ } else {
566- "solution", "Check certificate files and permissions in the certificate directory")581+ logger.Error("HTTPS server failed", "error", err, "address", httpsServer.Addr)
582+ }
567 }583 }
568- }584+ }()
569- }()585+ p.serverStarted = true // Mark server as started
586+ }
570 587
571- // Test admin connection after server starts588+ // Test admin connection after attempting to start the appropriate server
572 go p.TestAdminConnectionLater()589 go p.TestAdminConnectionLater()
573 590
574- return nil591+ // Remove the old/duplicate HTTP listener logic that was here
592+
593+ return nil // Return nil indicating server start was initiated
575 }594 }
576 595
577 // getTLSCertificateFunc returns a function that gets certificates for TLS handshakes596 // getTLSCertificateFunc returns a function that gets certificates for TLS handshakes
M internal/templating/models/templ/components/createcontainer.templ +8 -6
@@ -1,5 +1,7 @@
11 package components
22
3+import "fmt"
4+
35 // ContainerFormData holds the data needed for the container creation form
46 type ContainerFormData struct {
57 ImageName string
@@ -17,7 +19,7 @@ type ContainerFormData struct {
1719
1820 // CreateContainerForm displays a form for creating a new container
1921 templ CreateContainerForm(data ContainerFormData) {
20- <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets">
22+ <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow">
2123 <!-- Left Side: Docker Run Options -->
2224 <div class="flex-grow card p-6">
2325 <h2 class="card-title pb-4">Deploy a container based on { data.ImageName }</h2>
@@ -27,11 +29,12 @@ templ CreateContainerForm(data ContainerFormData) {
2729 <span>{ data.ErrorMessage }</span>
2830 </div>
2931 }
30- <form id={ "create-container-form-" + data.ShortID } action={ templ.SafeURL("/create-container/" + data.ShortID) } method="post"
31- hx-post={ "/htmx/create-container/" + data.ShortID }
32+ <form id={ "create-container-form-" + data.ShortID }
33+ action={ templ.URL(fmt.Sprintf("/create-container/%s", data.ShortID)) }
34+ method="post"
35+ hx-post={ string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", data.ShortID))) }
3236 hx-swap="innerHTML"
33- hx-target={ "#create-container-" + data.ShortID }
34- hx-target-*={ "#error-message-create-container-" + data.ShortID }>
37+ hx-target={ "#create-form-target-" + data.ShortID }>
3538 <!-- Image Name -->
3639 <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4">
3740 <div class="md:col-span-1">
@@ -154,7 +157,6 @@ templ CreateContainerForm(data ContainerFormData) {
154157 Create Container
155158 </button>
156159 </form>
157- <div id={ "error-message-create-container-" + data.ShortID } class="text-error mt-4"></div>
158160 </div>
159161 </div>
160162 } ⏎̸
@@ -1,5 +1,7 @@
1 package components1 package components
2 2
3+import "fmt"
4+
3 // ContainerFormData holds the data needed for the container creation form5 // ContainerFormData holds the data needed for the container creation form
4 type ContainerFormData struct {6 type ContainerFormData struct {
5 ImageName string7 ImageName string
@@ -17,7 +19,7 @@ type ContainerFormData struct {
17 19
18 // CreateContainerForm displays a form for creating a new container20 // CreateContainerForm displays a form for creating a new container
19 templ CreateContainerForm(data ContainerFormData) {21 templ CreateContainerForm(data ContainerFormData) {
20- <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow" hx-ext="response-targets">22+ <div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow">
21 <!-- Left Side: Docker Run Options -->23 <!-- Left Side: Docker Run Options -->
22 <div class="flex-grow card p-6">24 <div class="flex-grow card p-6">
23 <h2 class="card-title pb-4">Deploy a container based on { data.ImageName }</h2>25 <h2 class="card-title pb-4">Deploy a container based on { data.ImageName }</h2>
@@ -27,11 +29,12 @@ templ CreateContainerForm(data ContainerFormData) {
27 <span>{ data.ErrorMessage }</span>29 <span>{ data.ErrorMessage }</span>
28 </div>30 </div>
29 }31 }
30- <form id={ "create-container-form-" + data.ShortID } action={ templ.SafeURL("/create-container/" + data.ShortID) } method="post" 32+ <form id={ "create-container-form-" + data.ShortID }
31- hx-post={ "/htmx/create-container/" + data.ShortID }33+ action={ templ.URL(fmt.Sprintf("/create-container/%s", data.ShortID)) }
34+ method="post"
35+ hx-post={ string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", data.ShortID))) }
32 hx-swap="innerHTML" 36 hx-swap="innerHTML"
33- hx-target={ "#create-container-" + data.ShortID }37+ hx-target={ "#create-form-target-" + data.ShortID }>
34- hx-target-*={ "#error-message-create-container-" + data.ShortID }>
35 <!-- Image Name -->38 <!-- Image Name -->
36 <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4">39 <div class="grid grid-cols-1 md:grid-cols-3 gap-4 pb-4">
37 <div class="md:col-span-1">40 <div class="md:col-span-1">
@@ -154,7 +157,6 @@ templ CreateContainerForm(data ContainerFormData) {
154 Create Container157 Create Container
155 </button>158 </button>
156 </form>159 </form>
157- <div id={ "error-message-create-container-" + data.ShortID } class="text-error mt-4"></div>
158 </div>160 </div>
159 </div>161 </div>
160 } ⏎̸162 } ⏎̸
M internal/templating/models/templ/components/createcontainer_templ.go +39 -63
@@ -8,6 +8,8 @@ package components
88 import "github.com/a-h/templ"
99 import templruntime "github.com/a-h/templ/runtime"
1010
11+import "fmt"
12+
1113 // ContainerFormData holds the data needed for the container creation form
1214 type ContainerFormData struct {
1315 ImageName string
@@ -45,14 +47,14 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
4547 templ_7745c5c3_Var1 = templ.NopComponent
4648 }
4749 ctx = templ.ClearChildren(ctx)
48- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow\" hx-ext=\"response-targets\"><!-- Left Side: Docker Run Options --><div class=\"flex-grow card p-6\"><h2 class=\"card-title pb-4\">Deploy a container based on ")
50+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow\"><!-- Left Side: Docker Run Options --><div class=\"flex-grow card p-6\"><h2 class=\"card-title pb-4\">Deploy a container based on ")
4951 if templ_7745c5c3_Err != nil {
5052 return templ_7745c5c3_Err
5153 }
5254 var templ_7745c5c3_Var2 string
5355 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)
5456 if templ_7745c5c3_Err != nil {
55- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 23, Col: 75}
57+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 25, Col: 75}
5658 }
5759 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
5860 if templ_7745c5c3_Err != nil {
@@ -70,7 +72,7 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
7072 var templ_7745c5c3_Var3 string
7173 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
7274 if templ_7745c5c3_Err != nil {
73- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 27, Col: 30}
75+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 29, Col: 30}
7476 }
7577 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
7678 if templ_7745c5c3_Err != nil {
@@ -88,7 +90,7 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
8890 var templ_7745c5c3_Var4 string
8991 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("create-container-form-" + data.ShortID)
9092 if templ_7745c5c3_Err != nil {
91- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 30, Col: 53}
93+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 32, Col: 53}
9294 }
9395 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
9496 if templ_7745c5c3_Err != nil {
@@ -98,7 +100,7 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
98100 if templ_7745c5c3_Err != nil {
99101 return templ_7745c5c3_Err
100102 }
101- var templ_7745c5c3_Var5 templ.SafeURL = templ.SafeURL("/create-container/" + data.ShortID)
103+ var templ_7745c5c3_Var5 templ.SafeURL = templ.URL(fmt.Sprintf("/create-container/%s", data.ShortID))
102104 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
103105 if templ_7745c5c3_Err != nil {
104106 return templ_7745c5c3_Err
@@ -108,9 +110,9 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
108110 return templ_7745c5c3_Err
109111 }
110112 var templ_7745c5c3_Var6 string
111- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/htmx/create-container/" + data.ShortID)
113+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", data.ShortID))))
112114 if templ_7745c5c3_Err != nil {
113- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 31, Col: 54}
115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 35, Col: 87}
114116 }
115117 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
116118 if templ_7745c5c3_Err != nil {
@@ -121,158 +123,132 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
121123 return templ_7745c5c3_Err
122124 }
123125 var templ_7745c5c3_Var7 string
124- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("#create-container-" + data.ShortID)
126+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + data.ShortID)
125127 if templ_7745c5c3_Err != nil {
126- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 33, Col: 51}
128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 37, Col: 53}
127129 }
128130 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
129131 if templ_7745c5c3_Err != nil {
130132 return templ_7745c5c3_Err
131133 }
132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-target-*=\"")
134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><!-- Image Name --><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 pb-4\"><div class=\"md:col-span-1\"><label class=\"label\"><span class=\"label-text\">Image name</span></label> <input type=\"text\" name=\"image_name\" class=\"input input-bordered input-disabled w-full\" value=\"")
133135 if templ_7745c5c3_Err != nil {
134136 return templ_7745c5c3_Err
135137 }
136138 var templ_7745c5c3_Var8 string
137- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("#error-message-create-container-" + data.ShortID)
139+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)
138140 if templ_7745c5c3_Err != nil {
139- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 34, Col: 67}
141+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 45, Col: 29}
140142 }
141143 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
142144 if templ_7745c5c3_Err != nil {
143145 return templ_7745c5c3_Err
144146 }
145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"><!-- Image Name --><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 pb-4\"><div class=\"md:col-span-1\"><label class=\"label\"><span class=\"label-text\">Image name</span></label> <input type=\"text\" name=\"image_name\" class=\"input input-bordered input-disabled w-full\" value=\"")
147+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" readonly></div><div class=\"md:col-span-2\"><label class=\"label\"><span class=\"label-text\">Image ID</span></label> <input type=\"text\" name=\"image_id\" class=\"input input-bordered input-disabled w-full\" value=\"")
146148 if templ_7745c5c3_Err != nil {
147149 return templ_7745c5c3_Err
148150 }
149151 var templ_7745c5c3_Var9 string
150- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)
152+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageID)
151153 if templ_7745c5c3_Err != nil {
152- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 42, Col: 29}
154+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 53, Col: 27}
153155 }
154156 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
155157 if templ_7745c5c3_Err != nil {
156158 return templ_7745c5c3_Err
157159 }
158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" readonly></div><div class=\"md:col-span-2\"><label class=\"label\"><span class=\"label-text\">Image ID</span></label> <input type=\"text\" name=\"image_id\" class=\"input input-bordered input-disabled w-full\" value=\"")
160+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" readonly></div></div><!-- Container Name --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container name</span></label> <input type=\"text\" name=\"container_name\" class=\"input input-bordered w-full\" value=\"")
159161 if templ_7745c5c3_Err != nil {
160162 return templ_7745c5c3_Err
161163 }
162164 var templ_7745c5c3_Var10 string
163- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageID)
165+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerName)
164166 if templ_7745c5c3_Err != nil {
165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 50, Col: 27}
167+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 62, Col: 108}
166168 }
167169 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
168170 if templ_7745c5c3_Err != nil {
169171 return templ_7745c5c3_Err
170172 }
171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" readonly></div></div><!-- Container Name --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container name</span></label> <input type=\"text\" name=\"container_name\" class=\"input input-bordered w-full\" value=\"")
173+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"></div><!-- Http(s) / Subdomain / Domain --><div class=\"grid grid-cols-1 md:grid-cols-12 gap-4 pb-4\"><!-- Protocol --><div class=\"md:col-span-3\"><label class=\"label\"><span class=\"label-text\">Protocol</span></label> <select name=\"container_protocol\" class=\"select select-bordered w-full\"><option value=\"https\">https</option> <option value=\"http\">http</option></select></div><!-- Subdomain --><div class=\"md:col-span-4\"><label class=\"label\"><span class=\"label-text\">Subdomain</span></label> <input type=\"text\" name=\"container_subdomain\" class=\"input input-bordered w-full\" value=\"")
172174 if templ_7745c5c3_Err != nil {
173175 return templ_7745c5c3_Err
174176 }
175177 var templ_7745c5c3_Var11 string
176- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerName)
178+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerSubdomain)
177179 if templ_7745c5c3_Err != nil {
178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 59, Col: 108}
180+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 83, Col: 119}
179181 }
180182 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
181183 if templ_7745c5c3_Err != nil {
182184 return templ_7745c5c3_Err
183185 }
184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><!-- Http(s) / Subdomain / Domain --><div class=\"grid grid-cols-1 md:grid-cols-12 gap-4 pb-4\"><!-- Protocol --><div class=\"md:col-span-3\"><label class=\"label\"><span class=\"label-text\">Protocol</span></label> <select name=\"container_protocol\" class=\"select select-bordered w-full\"><option value=\"https\">https</option> <option value=\"http\">http</option></select></div><!-- Subdomain --><div class=\"md:col-span-4\"><label class=\"label\"><span class=\"label-text\">Subdomain</span></label> <input type=\"text\" name=\"container_subdomain\" class=\"input input-bordered w-full\" value=\"")
186+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><!-- Domain --><div class=\"md:col-span-5\"><label class=\"label\"><span class=\"label-text\">Domain</span></label> <input type=\"text\" name=\"container_domain\" class=\"input input-bordered w-full\" value=\"")
185187 if templ_7745c5c3_Err != nil {
186188 return templ_7745c5c3_Err
187189 }
188190 var templ_7745c5c3_Var12 string
189- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerSubdomain)
191+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerDomain)
190192 if templ_7745c5c3_Err != nil {
191- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 80, Col: 119}
193+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 91, Col: 113}
192194 }
193195 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
194196 if templ_7745c5c3_Err != nil {
195197 return templ_7745c5c3_Err
196198 }
197- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></div><!-- Domain --><div class=\"md:col-span-5\"><label class=\"label\"><span class=\"label-text\">Domain</span></label> <input type=\"text\" name=\"container_domain\" class=\"input input-bordered w-full\" value=\"")
199+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></div></div><!-- Exposed Ports --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Exposed Ports (Optional)</span> <span class=\"label-text-alt\">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span></label> <input type=\"text\" name=\"ports\" class=\"input input-bordered w-full\" value=\"")
198200 if templ_7745c5c3_Err != nil {
199201 return templ_7745c5c3_Err
200202 }
201203 var templ_7745c5c3_Var13 string
202- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerDomain)
204+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.Ports)
203205 if templ_7745c5c3_Err != nil {
204- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 88, Col: 113}
206+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 101, Col: 91}
205207 }
206208 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
207209 if templ_7745c5c3_Err != nil {
208210 return templ_7745c5c3_Err
209211 }
210- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div></div><!-- Exposed Ports --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Exposed Ports (Optional)</span> <span class=\"label-text-alt\">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span></label> <input type=\"text\" name=\"ports\" class=\"input input-bordered w-full\" value=\"")
212+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div><!-- Proxy Port --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container Proxy Port (Required)</span> <span class=\"label-text-alt\">Specify the internal container port that the proxy will route traffic to</span></label> <input type=\"text\" name=\"container_port\" class=\"input input-bordered w-full\" value=\"")
211213 if templ_7745c5c3_Err != nil {
212214 return templ_7745c5c3_Err
213215 }
214216 var templ_7745c5c3_Var14 string
215- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.Ports)
217+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.ProxyPort)
216218 if templ_7745c5c3_Err != nil {
217- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 98, Col: 91}
219+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 110, Col: 104}
218220 }
219221 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
220222 if templ_7745c5c3_Err != nil {
221223 return templ_7745c5c3_Err
222224 }
223- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"></div><!-- Proxy Port --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container Proxy Port (Required)</span> <span class=\"label-text-alt\">Specify the internal container port that the proxy will route traffic to</span></label> <input type=\"text\" name=\"container_port\" class=\"input input-bordered w-full\" value=\"")
225+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" required></div><!-- Volumes --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Volumes</span> <span class=\"label-text-alt\">e.g. ./data:/container/data</span></label> <input type=\"text\" name=\"volumes\" class=\"input input-bordered w-full\" value=\"")
224226 if templ_7745c5c3_Err != nil {
225227 return templ_7745c5c3_Err
226228 }
227229 var templ_7745c5c3_Var15 string
228- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.ProxyPort)
230+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volumes)
229231 if templ_7745c5c3_Err != nil {
230- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 107, Col: 104}
232+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 119, Col: 95}
231233 }
232234 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
233235 if templ_7745c5c3_Err != nil {
234236 return templ_7745c5c3_Err
235237 }
236- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" required></div><!-- Volumes --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Volumes</span> <span class=\"label-text-alt\">e.g. ./data:/container/data</span></label> <input type=\"text\" name=\"volumes\" class=\"input input-bordered w-full\" value=\"")
238+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"></div><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Environment variables</span> <span class=\"label-text-alt\">e.g. KEY=VALUE</span></label> <textarea name=\"environment_variables\" class=\"textarea textarea-bordered w-full\" rows=\"4\">")
237239 if templ_7745c5c3_Err != nil {
238240 return templ_7745c5c3_Err
239241 }
240242 var templ_7745c5c3_Var16 string
241- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volumes)
243+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.EnvironmentVariables)
242244 if templ_7745c5c3_Err != nil {
243- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 116, Col: 95}
245+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 128, Col: 42}
244246 }
245247 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
246248 if templ_7745c5c3_Err != nil {
247249 return templ_7745c5c3_Err
248250 }
249- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></div><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Environment variables</span> <span class=\"label-text-alt\">e.g. KEY=VALUE</span></label> <textarea name=\"environment_variables\" class=\"textarea textarea-bordered w-full\" rows=\"4\">")
250- if templ_7745c5c3_Err != nil {
251- return templ_7745c5c3_Err
252- }
253- var templ_7745c5c3_Var17 string
254- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.EnvironmentVariables)
255- if templ_7745c5c3_Err != nil {
256- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 125, Col: 42}
257- }
258- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
259- if templ_7745c5c3_Err != nil {
260- return templ_7745c5c3_Err
261- }
262- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</textarea></div><!-- Restart: always/on-failure/only when stopped/never --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Restart</span></label> <select name=\"restart\" class=\"select select-bordered w-full\"><option value=\"always\">Always</option> <option value=\"on-failure\">On failure</option> <option value=\"unless-stopped\">Unless stopped</option> <option value=\"never\">Never</option></select></div><!-- Skip Proxy Setup Option --><div class=\"form-control pb-4\"><label class=\"label cursor-pointer justify-start gap-4\"><input type=\"checkbox\" name=\"skip_proxy_setup\" value=\"true\" class=\"checkbox checkbox-primary\"> <span>Skip proxy setup (create container only, don't set up domain/certificates)</span></label><div class=\"text-sm opacity-70 ml-10\">Use this option if you only want to create the container without triggering proxy setup, certificate generation, and other related processes.</div></div><button type=\"submit\" id=\"create-container-submit\" class=\"btn btn-primary\">Create Container</button></form><div id=\"")
263- if templ_7745c5c3_Err != nil {
264- return templ_7745c5c3_Err
265- }
266- var templ_7745c5c3_Var18 string
267- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("error-message-create-container-" + data.ShortID)
268- if templ_7745c5c3_Err != nil {
269- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 157, Col: 61}
270- }
271- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
272- if templ_7745c5c3_Err != nil {
273- return templ_7745c5c3_Err
274- }
275- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-error mt-4\"></div></div></div>")
251+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea></div><!-- Restart: always/on-failure/only when stopped/never --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Restart</span></label> <select name=\"restart\" class=\"select select-bordered w-full\"><option value=\"always\">Always</option> <option value=\"on-failure\">On failure</option> <option value=\"unless-stopped\">Unless stopped</option> <option value=\"never\">Never</option></select></div><!-- Skip Proxy Setup Option --><div class=\"form-control pb-4\"><label class=\"label cursor-pointer justify-start gap-4\"><input type=\"checkbox\" name=\"skip_proxy_setup\" value=\"true\" class=\"checkbox checkbox-primary\"> <span>Skip proxy setup (create container only, don't set up domain/certificates)</span></label><div class=\"text-sm opacity-70 ml-10\">Use this option if you only want to create the container without triggering proxy setup, certificate generation, and other related processes.</div></div><button type=\"submit\" id=\"create-container-submit\" class=\"btn btn-primary\">Create Container</button></form></div></div>")
276252 if templ_7745c5c3_Err != nil {
277253 return templ_7745c5c3_Err
278254 }
@@ -8,6 +8,8 @@ package components
8 import "github.com/a-h/templ"8 import "github.com/a-h/templ"
9 import templruntime "github.com/a-h/templ/runtime"9 import templruntime "github.com/a-h/templ/runtime"
10 10
11+import "fmt"
12+
11 // ContainerFormData holds the data needed for the container creation form13 // ContainerFormData holds the data needed for the container creation form
12 type ContainerFormData struct {14 type ContainerFormData struct {
13 ImageName string15 ImageName string
@@ -45,14 +47,14 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
45 templ_7745c5c3_Var1 = templ.NopComponent47 templ_7745c5c3_Var1 = templ.NopComponent
46 }48 }
47 ctx = templ.ClearChildren(ctx)49 ctx = templ.ClearChildren(ctx)
48- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow\" hx-ext=\"response-targets\"><!-- Left Side: Docker Run Options --><div class=\"flex-grow card p-6\"><h2 class=\"card-title pb-4\">Deploy a container based on ")50+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4 flex-grow\"><!-- Left Side: Docker Run Options --><div class=\"flex-grow card p-6\"><h2 class=\"card-title pb-4\">Deploy a container based on ")
49 if templ_7745c5c3_Err != nil {51 if templ_7745c5c3_Err != nil {
50 return templ_7745c5c3_Err52 return templ_7745c5c3_Err
51 }53 }
52 var templ_7745c5c3_Var2 string54 var templ_7745c5c3_Var2 string
53 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)55 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)
54 if templ_7745c5c3_Err != nil {56 if templ_7745c5c3_Err != nil {
55- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 23, Col: 75}57+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 25, Col: 75}
56 }58 }
57 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))59 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
58 if templ_7745c5c3_Err != nil {60 if templ_7745c5c3_Err != nil {
@@ -70,7 +72,7 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
70 var templ_7745c5c3_Var3 string72 var templ_7745c5c3_Var3 string
71 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)73 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.ErrorMessage)
72 if templ_7745c5c3_Err != nil {74 if templ_7745c5c3_Err != nil {
73- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 27, Col: 30}75+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 29, Col: 30}
74 }76 }
75 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))77 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
76 if templ_7745c5c3_Err != nil {78 if templ_7745c5c3_Err != nil {
@@ -88,7 +90,7 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
88 var templ_7745c5c3_Var4 string90 var templ_7745c5c3_Var4 string
89 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("create-container-form-" + data.ShortID)91 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs("create-container-form-" + data.ShortID)
90 if templ_7745c5c3_Err != nil {92 if templ_7745c5c3_Err != nil {
91- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 30, Col: 53}93+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 32, Col: 53}
92 }94 }
93 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))95 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
94 if templ_7745c5c3_Err != nil {96 if templ_7745c5c3_Err != nil {
@@ -98,7 +100,7 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
98 if templ_7745c5c3_Err != nil {100 if templ_7745c5c3_Err != nil {
99 return templ_7745c5c3_Err101 return templ_7745c5c3_Err
100 }102 }
101- var templ_7745c5c3_Var5 templ.SafeURL = templ.SafeURL("/create-container/" + data.ShortID)103+ var templ_7745c5c3_Var5 templ.SafeURL = templ.URL(fmt.Sprintf("/create-container/%s", data.ShortID))
102 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))104 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
103 if templ_7745c5c3_Err != nil {105 if templ_7745c5c3_Err != nil {
104 return templ_7745c5c3_Err106 return templ_7745c5c3_Err
@@ -108,9 +110,9 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
108 return templ_7745c5c3_Err110 return templ_7745c5c3_Err
109 }111 }
110 var templ_7745c5c3_Var6 string112 var templ_7745c5c3_Var6 string
111- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("/htmx/create-container/" + data.ShortID)113+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", data.ShortID))))
112 if templ_7745c5c3_Err != nil {114 if templ_7745c5c3_Err != nil {
113- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 31, Col: 54}115+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 35, Col: 87}
114 }116 }
115 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))117 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
116 if templ_7745c5c3_Err != nil {118 if templ_7745c5c3_Err != nil {
@@ -121,158 +123,132 @@ func CreateContainerForm(data ContainerFormData) templ.Component {
121 return templ_7745c5c3_Err123 return templ_7745c5c3_Err
122 }124 }
123 var templ_7745c5c3_Var7 string125 var templ_7745c5c3_Var7 string
124- templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("#create-container-" + data.ShortID)126+ templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + data.ShortID)
125 if templ_7745c5c3_Err != nil {127 if templ_7745c5c3_Err != nil {
126- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 33, Col: 51}128+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 37, Col: 53}
127 }129 }
128 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))130 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
129 if templ_7745c5c3_Err != nil {131 if templ_7745c5c3_Err != nil {
130 return templ_7745c5c3_Err132 return templ_7745c5c3_Err
131 }133 }
132- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" hx-target-*=\"")134+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\"><!-- Image Name --><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 pb-4\"><div class=\"md:col-span-1\"><label class=\"label\"><span class=\"label-text\">Image name</span></label> <input type=\"text\" name=\"image_name\" class=\"input input-bordered input-disabled w-full\" value=\"")
133 if templ_7745c5c3_Err != nil {135 if templ_7745c5c3_Err != nil {
134 return templ_7745c5c3_Err136 return templ_7745c5c3_Err
135 }137 }
136 var templ_7745c5c3_Var8 string138 var templ_7745c5c3_Var8 string
137- templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs("#error-message-create-container-" + data.ShortID)139+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)
138 if templ_7745c5c3_Err != nil {140 if templ_7745c5c3_Err != nil {
139- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 34, Col: 67}141+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 45, Col: 29}
140 }142 }
141 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))143 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
142 if templ_7745c5c3_Err != nil {144 if templ_7745c5c3_Err != nil {
143 return templ_7745c5c3_Err145 return templ_7745c5c3_Err
144 }146 }
145- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"><!-- Image Name --><div class=\"grid grid-cols-1 md:grid-cols-3 gap-4 pb-4\"><div class=\"md:col-span-1\"><label class=\"label\"><span class=\"label-text\">Image name</span></label> <input type=\"text\" name=\"image_name\" class=\"input input-bordered input-disabled w-full\" value=\"")147+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" readonly></div><div class=\"md:col-span-2\"><label class=\"label\"><span class=\"label-text\">Image ID</span></label> <input type=\"text\" name=\"image_id\" class=\"input input-bordered input-disabled w-full\" value=\"")
146 if templ_7745c5c3_Err != nil {148 if templ_7745c5c3_Err != nil {
147 return templ_7745c5c3_Err149 return templ_7745c5c3_Err
148 }150 }
149 var templ_7745c5c3_Var9 string151 var templ_7745c5c3_Var9 string
150- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageName)152+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageID)
151 if templ_7745c5c3_Err != nil {153 if templ_7745c5c3_Err != nil {
152- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 42, Col: 29}154+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 53, Col: 27}
153 }155 }
154 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))156 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
155 if templ_7745c5c3_Err != nil {157 if templ_7745c5c3_Err != nil {
156 return templ_7745c5c3_Err158 return templ_7745c5c3_Err
157 }159 }
158- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" readonly></div><div class=\"md:col-span-2\"><label class=\"label\"><span class=\"label-text\">Image ID</span></label> <input type=\"text\" name=\"image_id\" class=\"input input-bordered input-disabled w-full\" value=\"")160+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" readonly></div></div><!-- Container Name --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container name</span></label> <input type=\"text\" name=\"container_name\" class=\"input input-bordered w-full\" value=\"")
159 if templ_7745c5c3_Err != nil {161 if templ_7745c5c3_Err != nil {
160 return templ_7745c5c3_Err162 return templ_7745c5c3_Err
161 }163 }
162 var templ_7745c5c3_Var10 string164 var templ_7745c5c3_Var10 string
163- templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.ImageID)165+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerName)
164 if templ_7745c5c3_Err != nil {166 if templ_7745c5c3_Err != nil {
165- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 50, Col: 27}167+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 62, Col: 108}
166 }168 }
167 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))169 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
168 if templ_7745c5c3_Err != nil {170 if templ_7745c5c3_Err != nil {
169 return templ_7745c5c3_Err171 return templ_7745c5c3_Err
170 }172 }
171- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" readonly></div></div><!-- Container Name --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container name</span></label> <input type=\"text\" name=\"container_name\" class=\"input input-bordered w-full\" value=\"")173+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"></div><!-- Http(s) / Subdomain / Domain --><div class=\"grid grid-cols-1 md:grid-cols-12 gap-4 pb-4\"><!-- Protocol --><div class=\"md:col-span-3\"><label class=\"label\"><span class=\"label-text\">Protocol</span></label> <select name=\"container_protocol\" class=\"select select-bordered w-full\"><option value=\"https\">https</option> <option value=\"http\">http</option></select></div><!-- Subdomain --><div class=\"md:col-span-4\"><label class=\"label\"><span class=\"label-text\">Subdomain</span></label> <input type=\"text\" name=\"container_subdomain\" class=\"input input-bordered w-full\" value=\"")
172 if templ_7745c5c3_Err != nil {174 if templ_7745c5c3_Err != nil {
173 return templ_7745c5c3_Err175 return templ_7745c5c3_Err
174 }176 }
175 var templ_7745c5c3_Var11 string177 var templ_7745c5c3_Var11 string
176- templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerName)178+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerSubdomain)
177 if templ_7745c5c3_Err != nil {179 if templ_7745c5c3_Err != nil {
178- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 59, Col: 108}180+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 83, Col: 119}
179 }181 }
180 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))182 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
181 if templ_7745c5c3_Err != nil {183 if templ_7745c5c3_Err != nil {
182 return templ_7745c5c3_Err184 return templ_7745c5c3_Err
183 }185 }
184- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><!-- Http(s) / Subdomain / Domain --><div class=\"grid grid-cols-1 md:grid-cols-12 gap-4 pb-4\"><!-- Protocol --><div class=\"md:col-span-3\"><label class=\"label\"><span class=\"label-text\">Protocol</span></label> <select name=\"container_protocol\" class=\"select select-bordered w-full\"><option value=\"https\">https</option> <option value=\"http\">http</option></select></div><!-- Subdomain --><div class=\"md:col-span-4\"><label class=\"label\"><span class=\"label-text\">Subdomain</span></label> <input type=\"text\" name=\"container_subdomain\" class=\"input input-bordered w-full\" value=\"")186+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><!-- Domain --><div class=\"md:col-span-5\"><label class=\"label\"><span class=\"label-text\">Domain</span></label> <input type=\"text\" name=\"container_domain\" class=\"input input-bordered w-full\" value=\"")
185 if templ_7745c5c3_Err != nil {187 if templ_7745c5c3_Err != nil {
186 return templ_7745c5c3_Err188 return templ_7745c5c3_Err
187 }189 }
188 var templ_7745c5c3_Var12 string190 var templ_7745c5c3_Var12 string
189- templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerSubdomain)191+ templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerDomain)
190 if templ_7745c5c3_Err != nil {192 if templ_7745c5c3_Err != nil {
191- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 80, Col: 119}193+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 91, Col: 113}
192 }194 }
193 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))195 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
194 if templ_7745c5c3_Err != nil {196 if templ_7745c5c3_Err != nil {
195 return templ_7745c5c3_Err197 return templ_7745c5c3_Err
196 }198 }
197- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></div><!-- Domain --><div class=\"md:col-span-5\"><label class=\"label\"><span class=\"label-text\">Domain</span></label> <input type=\"text\" name=\"container_domain\" class=\"input input-bordered w-full\" value=\"")199+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></div></div><!-- Exposed Ports --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Exposed Ports (Optional)</span> <span class=\"label-text-alt\">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span></label> <input type=\"text\" name=\"ports\" class=\"input input-bordered w-full\" value=\"")
198 if templ_7745c5c3_Err != nil {200 if templ_7745c5c3_Err != nil {
199 return templ_7745c5c3_Err201 return templ_7745c5c3_Err
200 }202 }
201 var templ_7745c5c3_Var13 string203 var templ_7745c5c3_Var13 string
202- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.ContainerDomain)204+ templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.Ports)
203 if templ_7745c5c3_Err != nil {205 if templ_7745c5c3_Err != nil {
204- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 88, Col: 113}206+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 101, Col: 91}
205 }207 }
206 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))208 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
207 if templ_7745c5c3_Err != nil {209 if templ_7745c5c3_Err != nil {
208 return templ_7745c5c3_Err210 return templ_7745c5c3_Err
209 }211 }
210- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div></div><!-- Exposed Ports --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Exposed Ports (Optional)</span> <span class=\"label-text-alt\">eg: 8080:1887/tcp (You can specify multiple ports by separating them with a comma)</span></label> <input type=\"text\" name=\"ports\" class=\"input input-bordered w-full\" value=\"")212+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div><!-- Proxy Port --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container Proxy Port (Required)</span> <span class=\"label-text-alt\">Specify the internal container port that the proxy will route traffic to</span></label> <input type=\"text\" name=\"container_port\" class=\"input input-bordered w-full\" value=\"")
211 if templ_7745c5c3_Err != nil {213 if templ_7745c5c3_Err != nil {
212 return templ_7745c5c3_Err214 return templ_7745c5c3_Err
213 }215 }
214 var templ_7745c5c3_Var14 string216 var templ_7745c5c3_Var14 string
215- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.Ports)217+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.ProxyPort)
216 if templ_7745c5c3_Err != nil {218 if templ_7745c5c3_Err != nil {
217- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 98, Col: 91}219+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 110, Col: 104}
218 }220 }
219 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))221 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
220 if templ_7745c5c3_Err != nil {222 if templ_7745c5c3_Err != nil {
221 return templ_7745c5c3_Err223 return templ_7745c5c3_Err
222 }224 }
223- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"></div><!-- Proxy Port --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Container Proxy Port (Required)</span> <span class=\"label-text-alt\">Specify the internal container port that the proxy will route traffic to</span></label> <input type=\"text\" name=\"container_port\" class=\"input input-bordered w-full\" value=\"")225+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" required></div><!-- Volumes --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Volumes</span> <span class=\"label-text-alt\">e.g. ./data:/container/data</span></label> <input type=\"text\" name=\"volumes\" class=\"input input-bordered w-full\" value=\"")
224 if templ_7745c5c3_Err != nil {226 if templ_7745c5c3_Err != nil {
225 return templ_7745c5c3_Err227 return templ_7745c5c3_Err
226 }228 }
227 var templ_7745c5c3_Var15 string229 var templ_7745c5c3_Var15 string
228- templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.ProxyPort)230+ templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volumes)
229 if templ_7745c5c3_Err != nil {231 if templ_7745c5c3_Err != nil {
230- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 107, Col: 104}232+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 119, Col: 95}
231 }233 }
232 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))234 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
233 if templ_7745c5c3_Err != nil {235 if templ_7745c5c3_Err != nil {
234 return templ_7745c5c3_Err236 return templ_7745c5c3_Err
235 }237 }
236- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" required></div><!-- Volumes --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Volumes</span> <span class=\"label-text-alt\">e.g. ./data:/container/data</span></label> <input type=\"text\" name=\"volumes\" class=\"input input-bordered w-full\" value=\"")238+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"></div><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Environment variables</span> <span class=\"label-text-alt\">e.g. KEY=VALUE</span></label> <textarea name=\"environment_variables\" class=\"textarea textarea-bordered w-full\" rows=\"4\">")
237 if templ_7745c5c3_Err != nil {239 if templ_7745c5c3_Err != nil {
238 return templ_7745c5c3_Err240 return templ_7745c5c3_Err
239 }241 }
240 var templ_7745c5c3_Var16 string242 var templ_7745c5c3_Var16 string
241- templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.Volumes)243+ templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(data.EnvironmentVariables)
242 if templ_7745c5c3_Err != nil {244 if templ_7745c5c3_Err != nil {
243- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 116, Col: 95}245+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 128, Col: 42}
244 }246 }
245 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))247 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
246 if templ_7745c5c3_Err != nil {248 if templ_7745c5c3_Err != nil {
247 return templ_7745c5c3_Err249 return templ_7745c5c3_Err
248 }250 }
249- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></div><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Environment variables</span> <span class=\"label-text-alt\">e.g. KEY=VALUE</span></label> <textarea name=\"environment_variables\" class=\"textarea textarea-bordered w-full\" rows=\"4\">")251+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</textarea></div><!-- Restart: always/on-failure/only when stopped/never --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Restart</span></label> <select name=\"restart\" class=\"select select-bordered w-full\"><option value=\"always\">Always</option> <option value=\"on-failure\">On failure</option> <option value=\"unless-stopped\">Unless stopped</option> <option value=\"never\">Never</option></select></div><!-- Skip Proxy Setup Option --><div class=\"form-control pb-4\"><label class=\"label cursor-pointer justify-start gap-4\"><input type=\"checkbox\" name=\"skip_proxy_setup\" value=\"true\" class=\"checkbox checkbox-primary\"> <span>Skip proxy setup (create container only, don't set up domain/certificates)</span></label><div class=\"text-sm opacity-70 ml-10\">Use this option if you only want to create the container without triggering proxy setup, certificate generation, and other related processes.</div></div><button type=\"submit\" id=\"create-container-submit\" class=\"btn btn-primary\">Create Container</button></form></div></div>")
250- if templ_7745c5c3_Err != nil {
251- return templ_7745c5c3_Err
252- }
253- var templ_7745c5c3_Var17 string
254- templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.EnvironmentVariables)
255- if templ_7745c5c3_Err != nil {
256- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 125, Col: 42}
257- }
258- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
259- if templ_7745c5c3_Err != nil {
260- return templ_7745c5c3_Err
261- }
262- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</textarea></div><!-- Restart: always/on-failure/only when stopped/never --><div class=\"form-control pb-4\"><label class=\"label\"><span class=\"label-text\">Restart</span></label> <select name=\"restart\" class=\"select select-bordered w-full\"><option value=\"always\">Always</option> <option value=\"on-failure\">On failure</option> <option value=\"unless-stopped\">Unless stopped</option> <option value=\"never\">Never</option></select></div><!-- Skip Proxy Setup Option --><div class=\"form-control pb-4\"><label class=\"label cursor-pointer justify-start gap-4\"><input type=\"checkbox\" name=\"skip_proxy_setup\" value=\"true\" class=\"checkbox checkbox-primary\"> <span>Skip proxy setup (create container only, don't set up domain/certificates)</span></label><div class=\"text-sm opacity-70 ml-10\">Use this option if you only want to create the container without triggering proxy setup, certificate generation, and other related processes.</div></div><button type=\"submit\" id=\"create-container-submit\" class=\"btn btn-primary\">Create Container</button></form><div id=\"")
263- if templ_7745c5c3_Err != nil {
264- return templ_7745c5c3_Err
265- }
266- var templ_7745c5c3_Var18 string
267- templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("error-message-create-container-" + data.ShortID)
268- if templ_7745c5c3_Err != nil {
269- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/createcontainer.templ`, Line: 157, Col: 61}
270- }
271- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
272- if templ_7745c5c3_Err != nil {
273- return templ_7745c5c3_Err
274- }
275- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-error mt-4\"></div></div></div>")
276 if templ_7745c5c3_Err != nil {252 if templ_7745c5c3_Err != nil {
277 return templ_7745c5c3_Err253 return templ_7745c5c3_Err
278 }254 }
M internal/templating/models/templ/components/footer.templ +7 -29
@@ -3,37 +3,15 @@ package components
33 // Footer is a templ component for the page footer
44 // It replaces footer.gohtml
55 templ Footer() {
6- <footer class="footer p-10 bg-base-200 text-base-content">
7- <div>
8- <span class="footer-title">Gordon</span>
9- <a class="link link-hover">About us</a>
10- <a class="link link-hover">Documentation</a>
11- <a class="link link-hover">Github</a>
12- </div>
13- <div>
14- <span class="footer-title">Container Management</span>
15- <a class="link link-hover">Dashboard</a>
16- <a class="link link-hover">Settings</a>
17- <a class="link link-hover">Stats</a>
18- </div>
19- <div>
20- <span class="footer-title">Legal</span>
21- <a class="link link-hover">Terms of use</a>
22- <a class="link link-hover">Privacy policy</a>
23- <a class="link link-hover">License</a>
24- </div>
25- </footer>
26- <footer class="footer px-10 py-4 border-t bg-base-300 text-base-content border-base-300">
6+ <footer class="footer items-center p-4 bg-base-300 text-base-content">
277 <div class="items-center grid-flow-col">
28- <span class="iconfb text-2xl">󰊢</span>
29- <p class="IBMPlexSans">Gordon Container Manager<br/>Providing reliable container management since 2023</p>
8+ <span class="iconfb text-xl">󰊢</span>
9+ <p class="IBMPlexSans text-sm">Gordon Container Manager © 2024</p>
3010 </div>
31- <div class="md:place-self-center md:justify-self-end">
32- <div class="grid grid-flow-col gap-4">
33- <a class="iconfb text-2xl">󰊤</a>
34- <a class="iconfb text-2xl">󰗃</a>
35- <a class="iconfb text-2xl">󰖟</a>
36- </div>
11+ <div class="grid-flow-col gap-4 md:place-self-center md:justify-self-end">
12+ <a class="link link-hover text-sm" href="https://github.com/bnema/gordon" target="_blank">GitHub</a>
13+ // Add other essential links here if needed, e.g., Documentation
14+ // <a class="link link-hover text-sm">Documentation</a>
3715 </div>
3816 </footer>
3917 }⏎̸
@@ -3,37 +3,15 @@ package components
3 // Footer is a templ component for the page footer3 // Footer is a templ component for the page footer
4 // It replaces footer.gohtml4 // It replaces footer.gohtml
5 templ Footer() {5 templ Footer() {
6- <footer class="footer p-10 bg-base-200 text-base-content">6+ <footer class="footer items-center p-4 bg-base-300 text-base-content">
7- <div>
8- <span class="footer-title">Gordon</span>
9- <a class="link link-hover">About us</a>
10- <a class="link link-hover">Documentation</a>
11- <a class="link link-hover">Github</a>
12- </div>
13- <div>
14- <span class="footer-title">Container Management</span>
15- <a class="link link-hover">Dashboard</a>
16- <a class="link link-hover">Settings</a>
17- <a class="link link-hover">Stats</a>
18- </div>
19- <div>
20- <span class="footer-title">Legal</span>
21- <a class="link link-hover">Terms of use</a>
22- <a class="link link-hover">Privacy policy</a>
23- <a class="link link-hover">License</a>
24- </div>
25- </footer>
26- <footer class="footer px-10 py-4 border-t bg-base-300 text-base-content border-base-300">
27 <div class="items-center grid-flow-col">7 <div class="items-center grid-flow-col">
28- <span class="iconfb text-2xl">󰊢</span> 8+ <span class="iconfb text-xl">󰊢</span>
29- <p class="IBMPlexSans">Gordon Container Manager<br/>Providing reliable container management since 2023</p>9+ <p class="IBMPlexSans text-sm">Gordon Container Manager © 2024</p>
30 </div> 10 </div>
31- <div class="md:place-self-center md:justify-self-end">11+ <div class="grid-flow-col gap-4 md:place-self-center md:justify-self-end">
32- <div class="grid grid-flow-col gap-4">12+ <a class="link link-hover text-sm" href="https://github.com/bnema/gordon" target="_blank">GitHub</a>
33- <a class="iconfb text-2xl">󰊤</a> 13+ // Add other essential links here if needed, e.g., Documentation
34- <a class="iconfb text-2xl">󰗃</a>14+ // <a class="link link-hover text-sm">Documentation</a>
35- <a class="iconfb text-2xl">󰖟</a>
36- </div>
37 </div>15 </div>
38 </footer>16 </footer>
39 }⏎̸17 }⏎̸
M internal/templating/models/templ/components/footer_templ.go +1 -1 generated file: collapsed
M internal/templating/models/templ/components/imagelist.templ +20 -3
@@ -2,6 +2,7 @@ package components
22
33 import (
44 "strconv"
5+ "fmt"
56 )
67
78 // HumanReadableImage represents an image with human-readable attributes
@@ -24,7 +25,7 @@ templ ImageList(images []HumanReadableImage) {
2425 <div class="card-body p-4">
2526
2627 <!-- Action buttons -->
27- <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">
28+ <div class="flex flex-col sm:flex-row sm:justify-start items-start sm:items-center gap-4 flex-wrap">
2829 <h3 class="font-semibold text-base-content">Available Images: { strconv.Itoa(len(images)) }</h3>
2930 <div class="flex gap-2">
3031 <button
@@ -41,6 +42,14 @@ templ ImageList(images []HumanReadableImage) {
4142 hx-swap="innerHTML">
4243 <span class="iconf">󰑓</span> Refresh
4344 </button>
45+ <button
46+ class="btn btn-sm btn-warning"
47+ hx-post="/htmx/image-manager/prune"
48+ hx-target="#image-manager"
49+ hx-swap="innerHTML"
50+ hx-confirm="Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.">
51+ <span class="iconf">󰩹</span> Purge Unused
52+ </button>
4453 </div>
4554 </div>
4655
@@ -90,7 +99,10 @@ templ ImageList(images []HumanReadableImage) {
9099 <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
91100 <li>
92101 <a
93- href={ templ.SafeURL("/htmx/create-container/" + img.ShortID) }
102+ href="#"
103+ hx-get={ string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID))) }
104+ hx-target={ "#create-form-target-" + img.ShortID }
105+ hx-swap="innerHTML"
94106 class="text-success flex items-center">
95107 <span class="iconf mr-2">󰐕</span> Create Container
96108 </a>
@@ -98,7 +110,7 @@ templ ImageList(images []HumanReadableImage) {
98110 <li>
99111 <button
100112 class="text-error flex items-center"
101- hx-delete={ "/htmx/image-manager/delete/" + img.ShortID }
113+ hx-delete={ string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID))) }
102114 hx-target="#image-manager"
103115 hx-swap="innerHTML"
104116 hx-confirm="Are you sure you want to delete this image?">
@@ -109,6 +121,11 @@ templ ImageList(images []HumanReadableImage) {
109121 </div>
110122 </th>
111123 </tr>
124+ <tr>
125+ <td colspan="5" id={ "create-form-target-" + img.ShortID }>
126+ <!-- Form will be loaded here by HTMX -->
127+ </td>
128+ </tr>
112129 }
113130 }
114131 </tbody>
@@ -2,6 +2,7 @@ package components
2 2
3 import (3 import (
4 "strconv"4 "strconv"
5+ "fmt"
5 )6 )
6 7
7 // HumanReadableImage represents an image with human-readable attributes8 // HumanReadableImage represents an image with human-readable attributes
@@ -24,7 +25,7 @@ templ ImageList(images []HumanReadableImage) {
24 <div class="card-body p-4">25 <div class="card-body p-4">
25 26
26 <!-- Action buttons -->27 <!-- Action buttons -->
27- <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4">28+ <div class="flex flex-col sm:flex-row sm:justify-start items-start sm:items-center gap-4 flex-wrap">
28 <h3 class="font-semibold text-base-content">Available Images: { strconv.Itoa(len(images)) }</h3>29 <h3 class="font-semibold text-base-content">Available Images: { strconv.Itoa(len(images)) }</h3>
29 <div class="flex gap-2">30 <div class="flex gap-2">
30 <button 31 <button
@@ -41,6 +42,14 @@ templ ImageList(images []HumanReadableImage) {
41 hx-swap="innerHTML">42 hx-swap="innerHTML">
42 <span class="iconf">󰑓</span> Refresh43 <span class="iconf">󰑓</span> Refresh
43 </button>44 </button>
45+ <button
46+ class="btn btn-sm btn-warning"
47+ hx-post="/htmx/image-manager/prune"
48+ hx-target="#image-manager"
49+ hx-swap="innerHTML"
50+ hx-confirm="Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.">
51+ <span class="iconf">󰩹</span> Purge Unused
52+ </button>
44 </div>53 </div>
45 </div>54 </div>
46 55
@@ -90,7 +99,10 @@ templ ImageList(images []HumanReadableImage) {
90 <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">99 <ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52">
91 <li>100 <li>
92 <a 101 <a
93- href={ templ.SafeURL("/htmx/create-container/" + img.ShortID) }102+ href="#"
103+ hx-get={ string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID))) }
104+ hx-target={ "#create-form-target-" + img.ShortID }
105+ hx-swap="innerHTML"
94 class="text-success flex items-center">106 class="text-success flex items-center">
95 <span class="iconf mr-2">󰐕</span> Create Container107 <span class="iconf mr-2">󰐕</span> Create Container
96 </a>108 </a>
@@ -98,7 +110,7 @@ templ ImageList(images []HumanReadableImage) {
98 <li>110 <li>
99 <button 111 <button
100 class="text-error flex items-center"112 class="text-error flex items-center"
101- hx-delete={ "/htmx/image-manager/delete/" + img.ShortID }113+ hx-delete={ string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID))) }
102 hx-target="#image-manager"114 hx-target="#image-manager"
103 hx-swap="innerHTML"115 hx-swap="innerHTML"
104 hx-confirm="Are you sure you want to delete this image?">116 hx-confirm="Are you sure you want to delete this image?">
@@ -109,6 +121,11 @@ templ ImageList(images []HumanReadableImage) {
109 </div>121 </div>
110 </th>122 </th>
111 </tr>123 </tr>
124+ <tr>
125+ <td colspan="5" id={ "create-form-target-" + img.ShortID }>
126+ <!-- Form will be loaded here by HTMX -->
127+ </td>
128+ </tr>
112 }129 }
113 }130 }
114 </tbody>131 </tbody>
M internal/templating/models/templ/components/imagelist_templ.go +47 -16 generated file: collapsed
M internal/templating/models/templ/components/navbar.templ +1 -1
@@ -3,7 +3,7 @@ package components
33 // Navbar is a menu component for navigation
44 templ Navbar(buildVersion string, adminPath string, userSettingsURL string, logoutURL string) {
55 <!-- DaisyUI Navbar -->
6- <div class="navbar bg-base-100 shadow-lg fixed top-0 z-50">
6+ <div class="navbar bg-base-200 shadow-lg fixed top-0 z-50">
77 <div class="navbar-start">
88 <div class="flex items-center">
99 <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4">
@@ -3,7 +3,7 @@ package components
3 // Navbar is a menu component for navigation3 // Navbar is a menu component for navigation
4 templ Navbar(buildVersion string, adminPath string, userSettingsURL string, logoutURL string) {4 templ Navbar(buildVersion string, adminPath string, userSettingsURL string, logoutURL string) {
5 <!-- DaisyUI Navbar -->5 <!-- DaisyUI Navbar -->
6- <div class="navbar bg-base-100 shadow-lg fixed top-0 z-50">6+ <div class="navbar bg-base-200 shadow-lg fixed top-0 z-50">
7 <div class="navbar-start">7 <div class="navbar-start">
8 <div class="flex items-center">8 <div class="flex items-center">
9 <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4">9 <h1 class="gordon-title text-2xl md:text-3xl text-primary tracking-tighter px-4">
M internal/templating/models/templ/components/navbar_templ.go +1 -1 generated file: collapsed
M internal/templating/models/templ/pages/admin/manager.templ +3 -3
@@ -17,7 +17,7 @@ templ ManagerPage(title string, buildVersion string, adminPath string, userSetti
1717 </div>
1818
1919 <!-- Stats overview -->
20- <div class="stats shadow mb-6 w-full">
20+ <div class="stats shadow mb-6 w-full bg-base-300">
2121 <div class="stat">
2222 <div class="stat-figure text-primary">
2323 <span class="iconf text-3xl">󰡨</span>
@@ -40,7 +40,7 @@ templ ManagerPage(title string, buildVersion string, adminPath string, userSetti
4040 <!-- Tabs for containers and images -->
4141 <div role="tablist" class="tabs tabs-boxed mb-4">
4242 <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked />
43- <div role="tabpanel" class="tab-content p-0">
43+ <div role="tabpanel" class="tab-content p-0 bg-base-300">
4444 <!-- Container manager section -->
4545 <div id="container-manager"
4646 hx-get="/htmx/container-manager"
@@ -55,7 +55,7 @@ templ ManagerPage(title string, buildVersion string, adminPath string, userSetti
5555 </div>
5656
5757 <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" />
58- <div role="tabpanel" class="tab-content p-0">
58+ <div role="tabpanel" class="tab-content p-0 bg-base-300">
5959 <!-- Image manager section -->
6060 <div id="image-manager"
6161 hx-get="/htmx/image-manager"
@@ -17,7 +17,7 @@ templ ManagerPage(title string, buildVersion string, adminPath string, userSetti
17 </div>17 </div>
18 18
19 <!-- Stats overview -->19 <!-- Stats overview -->
20- <div class="stats shadow mb-6 w-full">20+ <div class="stats shadow mb-6 w-full bg-base-300">
21 <div class="stat">21 <div class="stat">
22 <div class="stat-figure text-primary">22 <div class="stat-figure text-primary">
23 <span class="iconf text-3xl">󰡨</span>23 <span class="iconf text-3xl">󰡨</span>
@@ -40,7 +40,7 @@ templ ManagerPage(title string, buildVersion string, adminPath string, userSetti
40 <!-- Tabs for containers and images -->40 <!-- Tabs for containers and images -->
41 <div role="tablist" class="tabs tabs-boxed mb-4">41 <div role="tablist" class="tabs tabs-boxed mb-4">
42 <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked />42 <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Containers" id="containers-tab" checked />
43- <div role="tabpanel" class="tab-content p-0">43+ <div role="tabpanel" class="tab-content p-0 bg-base-300">
44 <!-- Container manager section -->44 <!-- Container manager section -->
45 <div id="container-manager"45 <div id="container-manager"
46 hx-get="/htmx/container-manager"46 hx-get="/htmx/container-manager"
@@ -55,7 +55,7 @@ templ ManagerPage(title string, buildVersion string, adminPath string, userSetti
55 </div>55 </div>
56 56
57 <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" />57 <input type="radio" name="my_tabs" role="tab" class="tab" aria-label="Images" id="images-tab" />
58- <div role="tabpanel" class="tab-content p-0">58+ <div role="tabpanel" class="tab-content p-0 bg-base-300">
59 <!-- Image manager section -->59 <!-- Image manager section -->
60 <div id="image-manager"60 <div id="image-manager"
61 hx-get="/htmx/image-manager"61 hx-get="/htmx/image-manager"
M internal/templating/models/templ/pages/admin/manager_templ.go +2 -2 generated file: collapsed
M internal/webui/public/assets/css/tailwind.css +9 -13
@@ -5886,10 +5886,6 @@
58865886 border-style: var(--tw-border-style);
58875887 border-width: 1px;
58885888 }
5889- .border-t {
5890- border-top-style: var(--tw-border-style);
5891- border-top-width: 1px;
5892- }
58935889 .btn-dash {
58945890 &:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked ) {
58955891 --btn-shadow: "";
@@ -5960,9 +5956,6 @@
59605956 color: var(--color-warning-content);
59615957 --alert-color: var(--color-warning);
59625958 }
5963- .border-base-300 {
5964- border-color: var(--color-base-300);
5965- }
59665959 .glass {
59675960 border: none;
59685961 backdrop-filter: blur(var(--glass-blur, 40px));
@@ -6201,9 +6194,6 @@
62016194 .p-6 {
62026195 padding: calc(var(--spacing) * 6);
62036196 }
6204- .p-10 {
6205- padding: calc(var(--spacing) * 10);
6206- }
62076197 .menu-title {
62086198 padding-inline: calc(0.25rem * 3);
62096199 padding-block: calc(0.25rem * 2);
@@ -6287,9 +6277,6 @@
62876277 .px-4 {
62886278 padding-inline: calc(var(--spacing) * 4);
62896279 }
6290- .px-10 {
6291- padding-inline: calc(var(--spacing) * 10);
6292- }
62936280 .py-4 {
62946281 padding-block: calc(var(--spacing) * 4);
62956282 }
@@ -6376,6 +6363,10 @@
63766363 font-size: var(--text-sm);
63776364 line-height: var(--tw-leading, var(--text-sm--line-height));
63786365 }
6366+ .text-xl {
6367+ font-size: var(--text-xl);
6368+ line-height: var(--tw-leading, var(--text-xl--line-height));
6369+ }
63796370 .text-xs {
63806371 font-size: var(--text-xs);
63816372 line-height: var(--tw-leading, var(--text-xs--line-height));
@@ -7511,6 +7502,11 @@
75117502 align-items: center;
75127503 }
75137504 }
7505+ .sm\:justify-start {
7506+ @media (width >= 40rem) {
7507+ justify-content: flex-start;
7508+ }
7509+ }
75147510 .md\:col-span-1 {
75157511 @media (width >= 48rem) {
75167512 grid-column: span 1 / span 1;
@@ -5886,10 +5886,6 @@
5886 border-style: var(--tw-border-style);5886 border-style: var(--tw-border-style);
5887 border-width: 1px;5887 border-width: 1px;
5888 }5888 }
5889- .border-t {
5890- border-top-style: var(--tw-border-style);
5891- border-top-width: 1px;
5892- }
5893 .btn-dash {5889 .btn-dash {
5894 &:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked ) {5890 &:not( .btn-active, :hover, :active:focus, :focus-visible, :disabled, [disabled], .btn-disabled, :checked ) {
5895 --btn-shadow: "";5891 --btn-shadow: "";
@@ -5960,9 +5956,6 @@
5960 color: var(--color-warning-content);5956 color: var(--color-warning-content);
5961 --alert-color: var(--color-warning);5957 --alert-color: var(--color-warning);
5962 }5958 }
5963- .border-base-300 {
5964- border-color: var(--color-base-300);
5965- }
5966 .glass {5959 .glass {
5967 border: none;5960 border: none;
5968 backdrop-filter: blur(var(--glass-blur, 40px));5961 backdrop-filter: blur(var(--glass-blur, 40px));
@@ -6201,9 +6194,6 @@
6201 .p-6 {6194 .p-6 {
6202 padding: calc(var(--spacing) * 6);6195 padding: calc(var(--spacing) * 6);
6203 }6196 }
6204- .p-10 {
6205- padding: calc(var(--spacing) * 10);
6206- }
6207 .menu-title {6197 .menu-title {
6208 padding-inline: calc(0.25rem * 3);6198 padding-inline: calc(0.25rem * 3);
6209 padding-block: calc(0.25rem * 2);6199 padding-block: calc(0.25rem * 2);
@@ -6287,9 +6277,6 @@
6287 .px-4 {6277 .px-4 {
6288 padding-inline: calc(var(--spacing) * 4);6278 padding-inline: calc(var(--spacing) * 4);
6289 }6279 }
6290- .px-10 {
6291- padding-inline: calc(var(--spacing) * 10);
6292- }
6293 .py-4 {6280 .py-4 {
6294 padding-block: calc(var(--spacing) * 4);6281 padding-block: calc(var(--spacing) * 4);
6295 }6282 }
@@ -6376,6 +6363,10 @@
6376 font-size: var(--text-sm);6363 font-size: var(--text-sm);
6377 line-height: var(--tw-leading, var(--text-sm--line-height));6364 line-height: var(--tw-leading, var(--text-sm--line-height));
6378 }6365 }
6366+ .text-xl {
6367+ font-size: var(--text-xl);
6368+ line-height: var(--tw-leading, var(--text-xl--line-height));
6369+ }
6379 .text-xs {6370 .text-xs {
6380 font-size: var(--text-xs);6371 font-size: var(--text-xs);
6381 line-height: var(--tw-leading, var(--text-xs--line-height));6372 line-height: var(--tw-leading, var(--text-xs--line-height));
@@ -7511,6 +7502,11 @@
7511 align-items: center;7502 align-items: center;
7512 }7503 }
7513 }7504 }
7505+ .sm\:justify-start {
7506+ @media (width >= 40rem) {
7507+ justify-content: flex-start;
7508+ }
7509+ }
7514 .md\:col-span-1 {7510 .md\:col-span-1 {
7515 @media (width >= 48rem) {7511 @media (width >= 48rem) {
7516 grid-column: span 1 / span 1;7512 grid-column: span 1 / span 1;
M pkg/docker/images.go +34 -1
@@ -13,8 +13,10 @@ import (
1313 "github.com/bnema/gordon/pkg/verify"
1414 "github.com/charmbracelet/log"
1515 "github.com/docker/docker/api/types"
16- "github.com/docker/docker/api/types/image"
16+ "github.com/docker/docker/api/types/filters"
17+ image "github.com/docker/docker/api/types/image"
1718 "github.com/docker/docker/client"
19+ "github.com/docker/go-units"
1820 )
1921
2022 func ListContainerImages() ([]image.Summary, error) {
@@ -325,3 +327,34 @@ func GetAllImages() ([]image.Summary, error) {
325327
326328 return images, nil
327329 }
330+
331+// PruneImages purges all unused (dangling) images from the Docker engine
332+func PruneImages() (uint64, error) {
333+ // Ensure the Docker client is initialized
334+ if err := CheckIfInitialized(); err != nil {
335+ return 0, fmt.Errorf("docker client not initialized: %w", err)
336+ }
337+
338+ ctx := context.Background()
339+ cli := dockerCli // Use the global client instance
340+
341+ // Define the filter to prune only dangling images (those without tags)
342+ pruneFilters := filters.NewArgs()
343+ pruneFilters.Add("dangling", "true")
344+
345+ // Prune the images
346+ report, err := cli.ImagesPrune(ctx, pruneFilters)
347+ if err != nil {
348+ log.Error("Failed to prune images", "error", err)
349+ return 0, fmt.Errorf("failed to prune images: %w", err)
350+ }
351+
352+ // Log the results
353+ log.Infof("Purged %d images, reclaimed %s space", len(report.ImagesDeleted), units.HumanSize(float64(report.SpaceReclaimed)))
354+ for _, img := range report.ImagesDeleted {
355+ log.Debugf("- Deleted: %s", img.Deleted)
356+ log.Debugf("- Untagged: %s", img.Untagged)
357+ }
358+
359+ return report.SpaceReclaimed, nil
360+}
@@ -13,8 +13,10 @@ import (
13 "github.com/bnema/gordon/pkg/verify"13 "github.com/bnema/gordon/pkg/verify"
14 "github.com/charmbracelet/log"14 "github.com/charmbracelet/log"
15 "github.com/docker/docker/api/types"15 "github.com/docker/docker/api/types"
16- "github.com/docker/docker/api/types/image"16+ "github.com/docker/docker/api/types/filters"
17+ image "github.com/docker/docker/api/types/image"
17 "github.com/docker/docker/client"18 "github.com/docker/docker/client"
19+ "github.com/docker/go-units"
18 )20 )
19 21
20 func ListContainerImages() ([]image.Summary, error) {22 func ListContainerImages() ([]image.Summary, error) {
@@ -325,3 +327,34 @@ func GetAllImages() ([]image.Summary, error) {
325 327
326 return images, nil328 return images, nil
327 }329 }
330+
331+// PruneImages purges all unused (dangling) images from the Docker engine
332+func PruneImages() (uint64, error) {
333+ // Ensure the Docker client is initialized
334+ if err := CheckIfInitialized(); err != nil {
335+ return 0, fmt.Errorf("docker client not initialized: %w", err)
336+ }
337+
338+ ctx := context.Background()
339+ cli := dockerCli // Use the global client instance
340+
341+ // Define the filter to prune only dangling images (those without tags)
342+ pruneFilters := filters.NewArgs()
343+ pruneFilters.Add("dangling", "true")
344+
345+ // Prune the images
346+ report, err := cli.ImagesPrune(ctx, pruneFilters)
347+ if err != nil {
348+ log.Error("Failed to prune images", "error", err)
349+ return 0, fmt.Errorf("failed to prune images: %w", err)
350+ }
351+
352+ // Log the results
353+ log.Infof("Purged %d images, reclaimed %s space", len(report.ImagesDeleted), units.HumanSize(float64(report.SpaceReclaimed)))
354+ for _, img := range report.ImagesDeleted {
355+ log.Debugf("- Deleted: %s", img.Deleted)
356+ log.Debugf("- Untagged: %s", img.Untagged)
357+ }
358+
359+ return report.SpaceReclaimed, nil
360+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close