detached ad19124

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
ad19124f7cd5c3f53163249c974741ef956e5b97
author
bamen · · 2025-03-30 12:46 UTC
parent
10909c5
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 generated file: collapsed
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
@@ -9,6 +9,7 @@ import "github.com/a-h/templ"
99 import templruntime "github.com/a-h/templ/runtime"
1010
1111 import (
12+ "fmt"
1213 "strconv"
1314 )
1415
@@ -47,20 +48,20 @@ func ImageList(images []HumanReadableImage) templ.Component {
4748 templ_7745c5c3_Var1 = templ.NopComponent
4849 }
4950 ctx = templ.ClearChildren(ctx)
50- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Image list using daisyUI components --><div class=\"card w-full bg-base-300\"><div class=\"card-body p-4\"><!-- Action buttons --><div class=\"flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4\"><h3 class=\"font-semibold text-base-content\">Available Images: ")
51+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Image list using daisyUI components --><div class=\"card w-full bg-base-300\"><div class=\"card-body p-4\"><!-- Action buttons --><div class=\"flex flex-col sm:flex-row sm:justify-start items-start sm:items-center gap-4 flex-wrap\"><h3 class=\"font-semibold text-base-content\">Available Images: ")
5152 if templ_7745c5c3_Err != nil {
5253 return templ_7745c5c3_Err
5354 }
5455 var templ_7745c5c3_Var2 string
5556 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(len(images)))
5657 if templ_7745c5c3_Err != nil {
57- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 28, Col: 93}
58+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 29, Col: 93}
5859 }
5960 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
6061 if templ_7745c5c3_Err != nil {
6162 return templ_7745c5c3_Err
6263 }
63- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>")
64+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button> <button class=\"btn btn-sm btn-warning\" hx-post=\"/htmx/image-manager/prune\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.\"><span class=\"iconf\">\U000f0a79</span> Purge Unused</button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>")
6465 if templ_7745c5c3_Err != nil {
6566 return templ_7745c5c3_Err
6667 }
@@ -78,7 +79,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
7879 var templ_7745c5c3_Var3 string
7980 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(img.Repository)
8081 if templ_7745c5c3_Err != nil {
81- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 77, Col: 50}
82+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 86, Col: 50}
8283 }
8384 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
8485 if templ_7745c5c3_Err != nil {
@@ -91,7 +92,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
9192 var templ_7745c5c3_Var4 string
9293 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(img.ID)
9394 if templ_7745c5c3_Err != nil {
94- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 78, Col: 51}
95+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 87, Col: 51}
9596 }
9697 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
9798 if templ_7745c5c3_Err != nil {
@@ -104,7 +105,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
104105 var templ_7745c5c3_Var5 string
105106 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(img.Tag)
106107 if templ_7745c5c3_Err != nil {
107- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 82, Col: 21}
108+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 91, Col: 21}
108109 }
109110 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
110111 if templ_7745c5c3_Err != nil {
@@ -117,7 +118,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
117118 var templ_7745c5c3_Var6 string
118119 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(img.Size)
119120 if templ_7745c5c3_Err != nil {
120- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 83, Col: 22}
121+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 92, Col: 22}
121122 }
122123 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
123124 if templ_7745c5c3_Err != nil {
@@ -130,41 +131,71 @@ func ImageList(images []HumanReadableImage) templ.Component {
130131 var templ_7745c5c3_Var7 string
131132 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Created)
132133 if templ_7745c5c3_Err != nil {
133- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 84, Col: 25}
134+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 93, Col: 25}
134135 }
135136 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
136137 if templ_7745c5c3_Err != nil {
137138 return templ_7745c5c3_Err
138139 }
139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><th><div class=\"dropdown dropdown-end\"><div tabindex=\"0\" role=\"button\" class=\"btn btn-ghost btn-xs\"><span class=\"iconf\">\U000f0140</span></div><ul tabindex=\"0\" class=\"dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52\"><li><a href=\"")
140+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><th><div class=\"dropdown dropdown-end\"><div tabindex=\"0\" role=\"button\" class=\"btn btn-ghost btn-xs\"><span class=\"iconf\">\U000f0140</span></div><ul tabindex=\"0\" class=\"dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52\"><li><a href=\"#\" hx-get=\"")
140141 if templ_7745c5c3_Err != nil {
141142 return templ_7745c5c3_Err
142143 }
143- var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL("/htmx/create-container/" + img.ShortID)
144- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
144+ var templ_7745c5c3_Var8 string
145+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID))))
146+ if templ_7745c5c3_Err != nil {
147+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 103, Col: 94}
148+ }
149+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
145150 if templ_7745c5c3_Err != nil {
146151 return templ_7745c5c3_Err
147152 }
148- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"text-success flex items-center\"><span class=\"iconf mr-2\">\U000f0415</span> Create Container</a></li><li><button class=\"text-error flex items-center\" hx-delete=\"")
153+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"")
149154 if templ_7745c5c3_Err != nil {
150155 return templ_7745c5c3_Err
151156 }
152157 var templ_7745c5c3_Var9 string
153- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("/htmx/image-manager/delete/" + img.ShortID)
158+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + img.ShortID)
154159 if templ_7745c5c3_Err != nil {
155- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 101, Col: 68}
160+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 104, Col: 61}
156161 }
157162 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
158163 if templ_7745c5c3_Err != nil {
159164 return templ_7745c5c3_Err
160165 }
161- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete this image?\"><span class=\"iconf mr-2\">\U000f01b4</span> Delete</button></li></ul></div></th></tr>")
166+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-swap=\"innerHTML\" class=\"text-success flex items-center\"><span class=\"iconf mr-2\">\U000f0415</span> Create Container</a></li><li><button class=\"text-error flex items-center\" hx-delete=\"")
167+ if templ_7745c5c3_Err != nil {
168+ return templ_7745c5c3_Err
169+ }
170+ var templ_7745c5c3_Var10 string
171+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID))))
172+ if templ_7745c5c3_Err != nil {
173+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 113, Col: 101}
174+ }
175+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
176+ if templ_7745c5c3_Err != nil {
177+ return templ_7745c5c3_Err
178+ }
179+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete this image?\"><span class=\"iconf mr-2\">\U000f01b4</span> Delete</button></li></ul></div></th></tr><tr><td colspan=\"5\" id=\"")
180+ if templ_7745c5c3_Err != nil {
181+ return templ_7745c5c3_Err
182+ }
183+ var templ_7745c5c3_Var11 string
184+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("create-form-target-" + img.ShortID)
185+ if templ_7745c5c3_Err != nil {
186+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 125, Col: 64}
187+ }
188+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
189+ if templ_7745c5c3_Err != nil {
190+ return templ_7745c5c3_Err
191+ }
192+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><!-- Form will be loaded here by HTMX --></td></tr>")
162193 if templ_7745c5c3_Err != nil {
163194 return templ_7745c5c3_Err
164195 }
165196 }
166197 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table></div></div><script>\n\tconsole.debug('Image list rendered');\n\t</script>")
198+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</tbody></table></div></div><script>\n\tconsole.debug('Image list rendered');\n\t</script>")
168199 if templ_7745c5c3_Err != nil {
169200 return templ_7745c5c3_Err
170201 }
@@ -9,6 +9,7 @@ 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 (11 import (
12+ "fmt"
12 "strconv"13 "strconv"
13 )14 )
14 15
@@ -47,20 +48,20 @@ func ImageList(images []HumanReadableImage) templ.Component {
47 templ_7745c5c3_Var1 = templ.NopComponent48 templ_7745c5c3_Var1 = templ.NopComponent
48 }49 }
49 ctx = templ.ClearChildren(ctx)50 ctx = templ.ClearChildren(ctx)
50- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Image list using daisyUI components --><div class=\"card w-full bg-base-300\"><div class=\"card-body p-4\"><!-- Action buttons --><div class=\"flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 mb-4\"><h3 class=\"font-semibold text-base-content\">Available Images: ")51+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!-- Image list using daisyUI components --><div class=\"card w-full bg-base-300\"><div class=\"card-body p-4\"><!-- Action buttons --><div class=\"flex flex-col sm:flex-row sm:justify-start items-start sm:items-center gap-4 flex-wrap\"><h3 class=\"font-semibold text-base-content\">Available Images: ")
51 if templ_7745c5c3_Err != nil {52 if templ_7745c5c3_Err != nil {
52 return templ_7745c5c3_Err53 return templ_7745c5c3_Err
53 }54 }
54 var templ_7745c5c3_Var2 string55 var templ_7745c5c3_Var2 string
55 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(len(images)))56 templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(len(images)))
56 if templ_7745c5c3_Err != nil {57 if templ_7745c5c3_Err != nil {
57- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 28, Col: 93}58+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 29, Col: 93}
58 }59 }
59 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))60 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
60 if templ_7745c5c3_Err != nil {61 if templ_7745c5c3_Err != nil {
61 return templ_7745c5c3_Err62 return templ_7745c5c3_Err
62 }63 }
63- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>")64+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h3><div class=\"flex gap-2\"><button class=\"btn btn-sm btn-secondary\" hx-get=\"/htmx/upload-image\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0992</span> Upload</button> <button class=\"btn btn-sm btn-primary\" hx-get=\"/htmx/image-manager\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\"><span class=\"iconf\">\U000f0453</span> Refresh</button> <button class=\"btn btn-sm btn-warning\" hx-post=\"/htmx/image-manager/prune\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete all unused (dangling and unreferenced) images? This cannot be undone.\"><span class=\"iconf\">\U000f0a79</span> Purge Unused</button></div></div><!-- Image list --><table class=\"table\"><!-- head --><thead><tr><th>Repository</th><th>Tag</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead> <tbody>")
64 if templ_7745c5c3_Err != nil {65 if templ_7745c5c3_Err != nil {
65 return templ_7745c5c3_Err66 return templ_7745c5c3_Err
66 }67 }
@@ -78,7 +79,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
78 var templ_7745c5c3_Var3 string79 var templ_7745c5c3_Var3 string
79 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(img.Repository)80 templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(img.Repository)
80 if templ_7745c5c3_Err != nil {81 if templ_7745c5c3_Err != nil {
81- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 77, Col: 50}82+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 86, Col: 50}
82 }83 }
83 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))84 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
84 if templ_7745c5c3_Err != nil {85 if templ_7745c5c3_Err != nil {
@@ -91,7 +92,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
91 var templ_7745c5c3_Var4 string92 var templ_7745c5c3_Var4 string
92 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(img.ID)93 templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(img.ID)
93 if templ_7745c5c3_Err != nil {94 if templ_7745c5c3_Err != nil {
94- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 78, Col: 51}95+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 87, Col: 51}
95 }96 }
96 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))97 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
97 if templ_7745c5c3_Err != nil {98 if templ_7745c5c3_Err != nil {
@@ -104,7 +105,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
104 var templ_7745c5c3_Var5 string105 var templ_7745c5c3_Var5 string
105 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(img.Tag)106 templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(img.Tag)
106 if templ_7745c5c3_Err != nil {107 if templ_7745c5c3_Err != nil {
107- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 82, Col: 21}108+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 91, Col: 21}
108 }109 }
109 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))110 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
110 if templ_7745c5c3_Err != nil {111 if templ_7745c5c3_Err != nil {
@@ -117,7 +118,7 @@ func ImageList(images []HumanReadableImage) templ.Component {
117 var templ_7745c5c3_Var6 string118 var templ_7745c5c3_Var6 string
118 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(img.Size)119 templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(img.Size)
119 if templ_7745c5c3_Err != nil {120 if templ_7745c5c3_Err != nil {
120- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 83, Col: 22}121+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 92, Col: 22}
121 }122 }
122 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))123 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
123 if templ_7745c5c3_Err != nil {124 if templ_7745c5c3_Err != nil {
@@ -130,41 +131,71 @@ func ImageList(images []HumanReadableImage) templ.Component {
130 var templ_7745c5c3_Var7 string131 var templ_7745c5c3_Var7 string
131 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Created)132 templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(img.Created)
132 if templ_7745c5c3_Err != nil {133 if templ_7745c5c3_Err != nil {
133- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 84, Col: 25}134+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 93, Col: 25}
134 }135 }
135 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))136 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
136 if templ_7745c5c3_Err != nil {137 if templ_7745c5c3_Err != nil {
137 return templ_7745c5c3_Err138 return templ_7745c5c3_Err
138 }139 }
139- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><th><div class=\"dropdown dropdown-end\"><div tabindex=\"0\" role=\"button\" class=\"btn btn-ghost btn-xs\"><span class=\"iconf\">\U000f0140</span></div><ul tabindex=\"0\" class=\"dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52\"><li><a href=\"")140+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><th><div class=\"dropdown dropdown-end\"><div tabindex=\"0\" role=\"button\" class=\"btn btn-ghost btn-xs\"><span class=\"iconf\">\U000f0140</span></div><ul tabindex=\"0\" class=\"dropdown-content z-[1] menu p-2 shadow bg-base-200 rounded-box w-52\"><li><a href=\"#\" hx-get=\"")
140 if templ_7745c5c3_Err != nil {141 if templ_7745c5c3_Err != nil {
141 return templ_7745c5c3_Err142 return templ_7745c5c3_Err
142 }143 }
143- var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL("/htmx/create-container/" + img.ShortID)144+ var templ_7745c5c3_Var8 string
144- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))145+ templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/create-container/%s", img.ShortID))))
146+ if templ_7745c5c3_Err != nil {
147+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 103, Col: 94}
148+ }
149+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
145 if templ_7745c5c3_Err != nil {150 if templ_7745c5c3_Err != nil {
146 return templ_7745c5c3_Err151 return templ_7745c5c3_Err
147 }152 }
148- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"text-success flex items-center\"><span class=\"iconf mr-2\">\U000f0415</span> Create Container</a></li><li><button class=\"text-error flex items-center\" hx-delete=\"")153+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-target=\"")
149 if templ_7745c5c3_Err != nil {154 if templ_7745c5c3_Err != nil {
150 return templ_7745c5c3_Err155 return templ_7745c5c3_Err
151 }156 }
152 var templ_7745c5c3_Var9 string157 var templ_7745c5c3_Var9 string
153- templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("/htmx/image-manager/delete/" + img.ShortID)158+ templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs("#create-form-target-" + img.ShortID)
154 if templ_7745c5c3_Err != nil {159 if templ_7745c5c3_Err != nil {
155- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 101, Col: 68}160+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 104, Col: 61}
156 }161 }
157 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))162 _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
158 if templ_7745c5c3_Err != nil {163 if templ_7745c5c3_Err != nil {
159 return templ_7745c5c3_Err164 return templ_7745c5c3_Err
160 }165 }
161- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete this image?\"><span class=\"iconf mr-2\">\U000f01b4</span> Delete</button></li></ul></div></th></tr>")166+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-swap=\"innerHTML\" class=\"text-success flex items-center\"><span class=\"iconf mr-2\">\U000f0415</span> Create Container</a></li><li><button class=\"text-error flex items-center\" hx-delete=\"")
167+ if templ_7745c5c3_Err != nil {
168+ return templ_7745c5c3_Err
169+ }
170+ var templ_7745c5c3_Var10 string
171+ templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/htmx/image-manager/delete/%s", img.ShortID))))
172+ if templ_7745c5c3_Err != nil {
173+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 113, Col: 101}
174+ }
175+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
176+ if templ_7745c5c3_Err != nil {
177+ return templ_7745c5c3_Err
178+ }
179+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" hx-target=\"#image-manager\" hx-swap=\"innerHTML\" hx-confirm=\"Are you sure you want to delete this image?\"><span class=\"iconf mr-2\">\U000f01b4</span> Delete</button></li></ul></div></th></tr><tr><td colspan=\"5\" id=\"")
180+ if templ_7745c5c3_Err != nil {
181+ return templ_7745c5c3_Err
182+ }
183+ var templ_7745c5c3_Var11 string
184+ templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("create-form-target-" + img.ShortID)
185+ if templ_7745c5c3_Err != nil {
186+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templating/models/templ/components/imagelist.templ`, Line: 125, Col: 64}
187+ }
188+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
189+ if templ_7745c5c3_Err != nil {
190+ return templ_7745c5c3_Err
191+ }
192+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"><!-- Form will be loaded here by HTMX --></td></tr>")
162 if templ_7745c5c3_Err != nil {193 if templ_7745c5c3_Err != nil {
163 return templ_7745c5c3_Err194 return templ_7745c5c3_Err
164 }195 }
165 }196 }
166 }197 }
167- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</tbody></table></div></div><script>\n\tconsole.debug('Image list rendered');\n\t</script>")198+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</tbody></table></div></div><script>\n\tconsole.debug('Image list rendered');\n\t</script>")
168 if templ_7745c5c3_Err != nil {199 if templ_7745c5c3_Err != nil {
169 return templ_7745c5c3_Err200 return templ_7745c5c3_Err
170 }201 }
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