refactor: Enhance static file serving and logging mechanisms
- Updated static file serving to conditionally use OS filesystem or embedded filesystem based on GORDON_LIVE_UI environment variable. - Improved logging for static file serving to provide clarity on the source of served files. - Refactored session middleware application to ensure it runs after static file handling. - Implemented a log cooldown mechanism for blocking direct IP access attempts, reducing log noise while maintaining security. - Updated template components to reference assets from the new serving path for consistency.
- commit
- db1fafa779eaf0cab20d49e48f6122c6214b3dc3
- author
- bamen · · 2025-03-30 18:40 UTC
- parent
- 8791634
- signature
- unsigned
7 files · +95 -38
M internal/httpserve/router.go +35 -21
| @@ -1,13 +1,15 @@ | |||
| 1 | 1 | package httpserve | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "io/fs" | |
| 4 | 5 | "net/http" | |
| 5 | 6 | "os" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/gordon/internal/httpserve/handlers" | |
| 8 | 9 | "github.com/bnema/gordon/internal/httpserve/middleware" | |
| 9 | 10 | "github.com/bnema/gordon/internal/server" | |
| 10 | - | "github.com/charmbracelet/log" | |
| 11 | + | "github.com/bnema/gordon/internal/webui" // Import the webui package | |
| 12 | + | log "github.com/bnema/gordon/pkg/logger" | |
| 11 | 13 | _ "github.com/joho/godotenv/autoload" | |
| 12 | 14 | "github.com/labstack/echo-contrib/session" | |
| 13 | 15 | "github.com/labstack/echo/v4" | |
| @@ -18,36 +20,31 @@ import ( | |||
| 18 | 20 | func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |
| 19 | 21 | e.Use(echomid.Recover()) | |
| 20 | 22 | ||
| 23 | + | // Determine if running in live UI mode | |
| 24 | + | useOS := os.Getenv("GORDON_LIVE_UI") == "true" | |
| 25 | + | staticFileSystem := getStaticFileSystem(useOS) | |
| 26 | + | assetHandler := http.FileServer(staticFileSystem) | |
| 27 | + | ||
| 28 | + | // Serve static files using the asset handler | |
| 29 | + | // We wrap the handler to ensure it works correctly with Echo | |
| 30 | + | // We also need to strip the prefix if accessing assets directly (e.g. /assets/css/...) | |
| 31 | + | // The root path "/" will serve index.html or equivalent by default if present. | |
| 32 | + | e.GET("/*", echo.WrapHandler(assetHandler)) | |
| 33 | + | ||
| 21 | 34 | // Log admin status | |
| 22 | 35 | log.Info("Admin WebUI status", | |
| 23 | 36 | "enabled", a.Config.Admin.IsAdminWebUIEnabled(), | |
| 24 | 37 | "path", a.Config.Admin.Path) | |
| 25 | 38 | ||
| 26 | 39 | // Initiate the session middleware | |
| 27 | - | e.Use(middleware.InitSessionMiddleware(a)) | |
| 40 | + | sessionMiddleware := middleware.InitSessionMiddleware(a) | |
| 41 | + | // Apply session middleware *after* the static file handler setup | |
| 42 | + | // It will only run for routes not handled by the static file server | |
| 43 | + | e.Use(sessionMiddleware) | |
| 28 | 44 | ||
| 29 | 45 | // Language detection | |
| 30 | 46 | e.Use(middleware.LanguageDetection) | |
| 31 | 47 | ||
| 32 | - | log.Debug("Binding static routes under /assets/*") | |
| 33 | - | // Middleware to set cache headers for static files | |
| 34 | - | staticCacheMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 35 | - | return func(c echo.Context) error { | |
| 36 | - | // Determine caching based on environment | |
| 37 | - | if os.Getenv("RUN_ENV") != "dev" { | |
| 38 | - | c.Response().Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day in production | |
| 39 | - | } else { | |
| 40 | - | c.Response().Header().Set("Cache-Control", "no-cache") // No caching in development | |
| 41 | - | } | |
| 42 | - | return next(c) | |
| 43 | - | } | |
| 44 | - | } | |
| 45 | - | // Use Echo's static file middleware to serve assets directly from the filesystem. | |
| 46 | - | // The path is relative to the project root. | |
| 47 | - | staticGroup := e.Group("/assets") | |
| 48 | - | staticGroup.Use(staticCacheMiddleware) | |
| 49 | - | staticGroup.Static("/", "internal/webui/public/assets") | |
| 50 | - | ||
| 51 | 48 | AdminPath := a.Config.Admin.Path | |
| 52 | 49 | log.Debug("Binding login routes with AdminPath", "path", AdminPath) | |
| 53 | 50 | bindLoginRoute(e, a, AdminPath) | |
| @@ -278,3 +275,20 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) { | |||
| 278 | 275 | return handlers.CreateContainerPOST(c, a) | |
| 279 | 276 | }) | |
| 280 | 277 | } | |
| 278 | + | ||
| 279 | + | // getStaticFileSystem returns the appropriate http.FileSystem based on the GORDON_LIVE_UI env var. | |
| 280 | + | func getStaticFileSystem(useOS bool) http.FileSystem { | |
| 281 | + | if useOS { | |
| 282 | + | log.Info("Serving static files from OS filesystem (live mode)") | |
| 283 | + | return http.FS(os.DirFS("internal/webui/public")) | |
| 284 | + | } | |
| 285 | + | ||
| 286 | + | log.Info("Serving static files from embedded filesystem") | |
| 287 | + | // Use the PublicFS from the webui package | |
| 288 | + | fsys, err := fs.Sub(webui.PublicFS, "public") | |
| 289 | + | if err != nil { | |
| 290 | + | panic(err) // Should not happen with embedded files | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | return http.FS(fsys) | |
| 294 | + | } | |
| @@ -1,13 +1,15 @@ | |||||
| 1 | package httpserve | 1 | package httpserve | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "io/fs" | |||
| 4 | "net/http" | 5 | "net/http" | ||
| 5 | "os" | 6 | "os" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/gordon/internal/httpserve/handlers" | 8 | "github.com/bnema/gordon/internal/httpserve/handlers" | ||
| 8 | "github.com/bnema/gordon/internal/httpserve/middleware" | 9 | "github.com/bnema/gordon/internal/httpserve/middleware" | ||
| 9 | "github.com/bnema/gordon/internal/server" | 10 | "github.com/bnema/gordon/internal/server" | ||
| 10 | - | "github.com/charmbracelet/log" | 11 | + | "github.com/bnema/gordon/internal/webui" // Import the webui package |
| 12 | + | log "github.com/bnema/gordon/pkg/logger" | |||
| 11 | _ "github.com/joho/godotenv/autoload" | 13 | _ "github.com/joho/godotenv/autoload" | ||
| 12 | "github.com/labstack/echo-contrib/session" | 14 | "github.com/labstack/echo-contrib/session" | ||
| 13 | "github.com/labstack/echo/v4" | 15 | "github.com/labstack/echo/v4" | ||
| @@ -18,36 +20,31 @@ import ( | |||||
| 18 | func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | 20 | func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | ||
| 19 | e.Use(echomid.Recover()) | 21 | e.Use(echomid.Recover()) | ||
| 20 | 22 | ||||
| 23 | + | // Determine if running in live UI mode | |||
| 24 | + | useOS := os.Getenv("GORDON_LIVE_UI") == "true" | |||
| 25 | + | staticFileSystem := getStaticFileSystem(useOS) | |||
| 26 | + | assetHandler := http.FileServer(staticFileSystem) | |||
| 27 | + | ||||
| 28 | + | // Serve static files using the asset handler | |||
| 29 | + | // We wrap the handler to ensure it works correctly with Echo | |||
| 30 | + | // We also need to strip the prefix if accessing assets directly (e.g. /assets/css/...) | |||
| 31 | + | // The root path "/" will serve index.html or equivalent by default if present. | |||
| 32 | + | e.GET("/*", echo.WrapHandler(assetHandler)) | |||
| 33 | + | ||||
| 21 | // Log admin status | 34 | // Log admin status | ||
| 22 | log.Info("Admin WebUI status", | 35 | log.Info("Admin WebUI status", | ||
| 23 | "enabled", a.Config.Admin.IsAdminWebUIEnabled(), | 36 | "enabled", a.Config.Admin.IsAdminWebUIEnabled(), | ||
| 24 | "path", a.Config.Admin.Path) | 37 | "path", a.Config.Admin.Path) | ||
| 25 | 38 | ||||
| 26 | // Initiate the session middleware | 39 | // Initiate the session middleware | ||
| 27 | - | e.Use(middleware.InitSessionMiddleware(a)) | 40 | + | sessionMiddleware := middleware.InitSessionMiddleware(a) |
| 41 | + | // Apply session middleware *after* the static file handler setup | |||
| 42 | + | // It will only run for routes not handled by the static file server | |||
| 43 | + | e.Use(sessionMiddleware) | |||
| 28 | 44 | ||||
| 29 | // Language detection | 45 | // Language detection | ||
| 30 | e.Use(middleware.LanguageDetection) | 46 | e.Use(middleware.LanguageDetection) | ||
| 31 | 47 | ||||
| 32 | - | log.Debug("Binding static routes under /assets/*") | |||
| 33 | - | // Middleware to set cache headers for static files | |||
| 34 | - | staticCacheMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc { | |||
| 35 | - | return func(c echo.Context) error { | |||
| 36 | - | // Determine caching based on environment | |||
| 37 | - | if os.Getenv("RUN_ENV") != "dev" { | |||
| 38 | - | c.Response().Header().Set("Cache-Control", "public, max-age=86400") // Cache for 1 day in production | |||
| 39 | - | } else { | |||
| 40 | - | c.Response().Header().Set("Cache-Control", "no-cache") // No caching in development | |||
| 41 | - | } | |||
| 42 | - | return next(c) | |||
| 43 | - | } | |||
| 44 | - | } | |||
| 45 | - | // Use Echo's static file middleware to serve assets directly from the filesystem. | |||
| 46 | - | // The path is relative to the project root. | |||
| 47 | - | staticGroup := e.Group("/assets") | |||
| 48 | - | staticGroup.Use(staticCacheMiddleware) | |||
| 49 | - | staticGroup.Static("/", "internal/webui/public/assets") | |||
| 50 | - | ||||
| 51 | AdminPath := a.Config.Admin.Path | 48 | AdminPath := a.Config.Admin.Path | ||
| 52 | log.Debug("Binding login routes with AdminPath", "path", AdminPath) | 49 | log.Debug("Binding login routes with AdminPath", "path", AdminPath) | ||
| 53 | bindLoginRoute(e, a, AdminPath) | 50 | bindLoginRoute(e, a, AdminPath) | ||
| @@ -278,3 +275,20 @@ func bindHTMXEndpoints(e *echo.Echo, a *server.App) { | |||||
| 278 | return handlers.CreateContainerPOST(c, a) | 275 | return handlers.CreateContainerPOST(c, a) | ||
| 279 | }) | 276 | }) | ||
| 280 | } | 277 | } | ||
| 278 | + | ||||
| 279 | + | // getStaticFileSystem returns the appropriate http.FileSystem based on the GORDON_LIVE_UI env var. | |||
| 280 | + | func getStaticFileSystem(useOS bool) http.FileSystem { | |||
| 281 | + | if useOS { | |||
| 282 | + | log.Info("Serving static files from OS filesystem (live mode)") | |||
| 283 | + | return http.FS(os.DirFS("internal/webui/public")) | |||
| 284 | + | } | |||
| 285 | + | ||||
| 286 | + | log.Info("Serving static files from embedded filesystem") | |||
| 287 | + | // Use the PublicFS from the webui package | |||
| 288 | + | fsys, err := fs.Sub(webui.PublicFS, "public") | |||
| 289 | + | if err != nil { | |||
| 290 | + | panic(err) // Should not happen with embedded files | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | return http.FS(fsys) | |||
| 294 | + | } | |||
M internal/proxy/middleware.go +38 -4
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "net" | |
| 6 | 6 | "net/http" | |
| 7 | 7 | "strings" | |
| 8 | + | "sync" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/bnema/gordon/pkg/logger" | |
| @@ -22,6 +23,13 @@ var rateLimiterCleanup []func() | |||
| 22 | 23 | // RequestIDKey is the key used to store the request ID in the context | |
| 23 | 24 | const RequestIDKey = "request_id" | |
| 24 | 25 | ||
| 26 | + | // Cooldown period for logging direct IP access warnings | |
| 27 | + | const logCooldown = 1 * time.Minute | |
| 28 | + | ||
| 29 | + | // Map to store the last log time for each IP address attempting direct access | |
| 30 | + | var ipLogTimes = make(map[string]time.Time) | |
| 31 | + | var ipLogMutex sync.Mutex // Mutex to protect access to ipLogTimes | |
| 32 | + | ||
| 25 | 33 | // isHostIPAddress checks if the host part of a string (like "1.2.3.4:80" or "1.2.3.4") is a valid IP address. | |
| 26 | 34 | func isHostIPAddress(host string) bool { | |
| 27 | 35 | // Split host and port | |
| @@ -36,22 +44,48 @@ func isHostIPAddress(host string) bool { | |||
| 36 | 44 | } | |
| 37 | 45 | ||
| 38 | 46 | // blockDirectIPMiddleware rejects requests where the Host header is an IP address, | |
| 39 | - | // unless it's an ACME challenge request. | |
| 47 | + | // unless it's an ACME challenge request. It includes a log cooldown mechanism. | |
| 40 | 48 | func blockDirectIPMiddleware() echo.MiddlewareFunc { | |
| 41 | 49 | return func(next echo.HandlerFunc) echo.HandlerFunc { | |
| 42 | 50 | return func(c echo.Context) error { | |
| 43 | 51 | host := c.Request().Host | |
| 44 | 52 | path := c.Request().URL.Path | |
| 53 | + | realIP := c.RealIP() // Get the real IP once | |
| 45 | 54 | ||
| 46 | 55 | if isHostIPAddress(host) { | |
| 47 | 56 | // Allow ACME HTTP-01 challenges to proceed even via IP | |
| 48 | 57 | if strings.HasPrefix(path, "/.well-known/acme-challenge/") { | |
| 49 | - | logger.Debug("Allowing direct IP access for ACME challenge", "host", host, "path", path, "ip", c.RealIP()) | |
| 58 | + | logger.Debug("Allowing direct IP access for ACME challenge", "host", host, "path", path, "ip", realIP) | |
| 50 | 59 | return next(c) | |
| 51 | 60 | } | |
| 52 | 61 | ||
| 53 | - | // Block other direct IP access attempts | |
| 54 | - | logger.Warn("Blocking direct IP access attempt", "host", host, "path", path, "ip", c.RealIP(), "user_agent", c.Request().UserAgent()) | |
| 62 | + | // --- Log Cooldown Logic --- | |
| 63 | + | now := time.Now() | |
| 64 | + | logAllowed := false | |
| 65 | + | ||
| 66 | + | ipLogMutex.Lock() | |
| 67 | + | lastLogTime, exists := ipLogTimes[realIP] | |
| 68 | + | if !exists || now.Sub(lastLogTime) > logCooldown { | |
| 69 | + | // It's a new IP or the cooldown has passed | |
| 70 | + | ipLogTimes[realIP] = now // Update the last log time | |
| 71 | + | logAllowed = true | |
| 72 | + | } | |
| 73 | + | ipLogMutex.Unlock() | |
| 74 | + | ||
| 75 | + | // Log only if allowed by the cooldown | |
| 76 | + | if logAllowed { | |
| 77 | + | logger.Warn("Blocking direct IP access attempt (log cooldown active)", | |
| 78 | + | "host", host, | |
| 79 | + | "path", path, | |
| 80 | + | "ip", realIP, | |
| 81 | + | "user_agent", c.Request().UserAgent()) | |
| 82 | + | } else { | |
| 83 | + | // Optionally log at DEBUG level that we suppressed a log, useful for verifying | |
| 84 | + | // logger.Debug("Suppressed duplicate direct IP access log due to cooldown", "ip", realIP, "host", host) | |
| 85 | + | } | |
| 86 | + | // --- End Log Cooldown Logic --- | |
| 87 | + | ||
| 88 | + | // Block other direct IP access attempts regardless of logging | |
| 55 | 89 | return echo.NewHTTPError(http.StatusForbidden, "Direct IP access is not permitted.") | |
| 56 | 90 | } | |
| 57 | 91 | ||
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "net" | 5 | "net" | ||
| 6 | "net/http" | 6 | "net/http" | ||
| 7 | "strings" | 7 | "strings" | ||
| 8 | + | "sync" | |||
| 8 | "time" | 9 | "time" | ||
| 9 | 10 | ||||
| 10 | "github.com/bnema/gordon/pkg/logger" | 11 | "github.com/bnema/gordon/pkg/logger" | ||
| @@ -22,6 +23,13 @@ var rateLimiterCleanup []func() | |||||
| 22 | // RequestIDKey is the key used to store the request ID in the context | 23 | // RequestIDKey is the key used to store the request ID in the context | ||
| 23 | const RequestIDKey = "request_id" | 24 | const RequestIDKey = "request_id" | ||
| 24 | 25 | ||||
| 26 | + | // Cooldown period for logging direct IP access warnings | |||
| 27 | + | const logCooldown = 1 * time.Minute | |||
| 28 | + | ||||
| 29 | + | // Map to store the last log time for each IP address attempting direct access | |||
| 30 | + | var ipLogTimes = make(map[string]time.Time) | |||
| 31 | + | var ipLogMutex sync.Mutex // Mutex to protect access to ipLogTimes | |||
| 32 | + | ||||
| 25 | // isHostIPAddress checks if the host part of a string (like "1.2.3.4:80" or "1.2.3.4") is a valid IP address. | 33 | // isHostIPAddress checks if the host part of a string (like "1.2.3.4:80" or "1.2.3.4") is a valid IP address. | ||
| 26 | func isHostIPAddress(host string) bool { | 34 | func isHostIPAddress(host string) bool { | ||
| 27 | // Split host and port | 35 | // Split host and port | ||
| @@ -36,22 +44,48 @@ func isHostIPAddress(host string) bool { | |||||
| 36 | } | 44 | } | ||
| 37 | 45 | ||||
| 38 | // blockDirectIPMiddleware rejects requests where the Host header is an IP address, | 46 | // blockDirectIPMiddleware rejects requests where the Host header is an IP address, | ||
| 39 | - | // unless it's an ACME challenge request. | 47 | + | // unless it's an ACME challenge request. It includes a log cooldown mechanism. |
| 40 | func blockDirectIPMiddleware() echo.MiddlewareFunc { | 48 | func blockDirectIPMiddleware() echo.MiddlewareFunc { | ||
| 41 | return func(next echo.HandlerFunc) echo.HandlerFunc { | 49 | return func(next echo.HandlerFunc) echo.HandlerFunc { | ||
| 42 | return func(c echo.Context) error { | 50 | return func(c echo.Context) error { | ||
| 43 | host := c.Request().Host | 51 | host := c.Request().Host | ||
| 44 | path := c.Request().URL.Path | 52 | path := c.Request().URL.Path | ||
| 53 | + | realIP := c.RealIP() // Get the real IP once | |||
| 45 | 54 | ||||
| 46 | if isHostIPAddress(host) { | 55 | if isHostIPAddress(host) { | ||
| 47 | // Allow ACME HTTP-01 challenges to proceed even via IP | 56 | // Allow ACME HTTP-01 challenges to proceed even via IP | ||
| 48 | if strings.HasPrefix(path, "/.well-known/acme-challenge/") { | 57 | if strings.HasPrefix(path, "/.well-known/acme-challenge/") { | ||
| 49 | - | logger.Debug("Allowing direct IP access for ACME challenge", "host", host, "path", path, "ip", c.RealIP()) | 58 | + | logger.Debug("Allowing direct IP access for ACME challenge", "host", host, "path", path, "ip", realIP) |
| 50 | return next(c) | 59 | return next(c) | ||
| 51 | } | 60 | } | ||
| 52 | 61 | ||||
| 53 | - | // Block other direct IP access attempts | 62 | + | // --- Log Cooldown Logic --- |
| 54 | - | logger.Warn("Blocking direct IP access attempt", "host", host, "path", path, "ip", c.RealIP(), "user_agent", c.Request().UserAgent()) | 63 | + | now := time.Now() |
| 64 | + | logAllowed := false | |||
| 65 | + | ||||
| 66 | + | ipLogMutex.Lock() | |||
| 67 | + | lastLogTime, exists := ipLogTimes[realIP] | |||
| 68 | + | if !exists || now.Sub(lastLogTime) > logCooldown { | |||
| 69 | + | // It's a new IP or the cooldown has passed | |||
| 70 | + | ipLogTimes[realIP] = now // Update the last log time | |||
| 71 | + | logAllowed = true | |||
| 72 | + | } | |||
| 73 | + | ipLogMutex.Unlock() | |||
| 74 | + | ||||
| 75 | + | // Log only if allowed by the cooldown | |||
| 76 | + | if logAllowed { | |||
| 77 | + | logger.Warn("Blocking direct IP access attempt (log cooldown active)", | |||
| 78 | + | "host", host, | |||
| 79 | + | "path", path, | |||
| 80 | + | "ip", realIP, | |||
| 81 | + | "user_agent", c.Request().UserAgent()) | |||
| 82 | + | } else { | |||
| 83 | + | // Optionally log at DEBUG level that we suppressed a log, useful for verifying | |||
| 84 | + | // logger.Debug("Suppressed duplicate direct IP access log due to cooldown", "ip", realIP, "host", host) | |||
| 85 | + | } | |||
| 86 | + | // --- End Log Cooldown Logic --- | |||
| 87 | + | ||||
| 88 | + | // Block other direct IP access attempts regardless of logging | |||
| 55 | return echo.NewHTTPError(http.StatusForbidden, "Direct IP access is not permitted.") | 89 | return echo.NewHTTPError(http.StatusForbidden, "Direct IP access is not permitted.") | ||
| 56 | } | 90 | } | ||
| 57 | 91 | ||||
M internal/templating/models/templ/components/header.templ +7 -7
| @@ -9,13 +9,13 @@ templ Header(title string) { | |||
| 9 | 9 | <title>{ title }</title> | |
| 10 | 10 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 11 | 11 | ||
| 12 | - | <script src="/assets/js/jquery.min.js"></script> | |
| 13 | - | <script src="/assets/js/htmx.min.js"></script> | |
| 14 | - | <script src="/assets/js/htmx-response-targets.js"></script> | |
| 15 | - | <script src="/assets/js/htmx-debug.js"></script> | |
| 16 | - | <script src="/assets/js/htmx-sse.js"></script> | |
| 17 | - | <script src="/assets/js/daisyui.js" type="module"></script> | |
| 18 | - | <script defer src="/assets/js/hyperscript.min.js"></script> | |
| 12 | + | <script src="/libs/jquery.min.js"></script> | |
| 13 | + | <script src="/libs/htmx.min.js"></script> | |
| 14 | + | <script src="/libs/htmx-response-targets.js"></script> | |
| 15 | + | <script src="/libs/htmx-debug.js"></script> | |
| 16 | + | <script src="/libs/htmx-sse.js"></script> | |
| 17 | + | <script src="/libs/daisyui.js" type="module"></script> | |
| 18 | + | <script defer src="/libs/hyperscript.min.js"></script> | |
| 19 | 19 | <script defer src="/assets/js/custom.js"></script> | |
| 20 | 20 | ||
| 21 | 21 | <!-- daisyUI theme configuration --> | |
| @@ -9,13 +9,13 @@ templ Header(title string) { | |||||
| 9 | <title>{ title }</title> | 9 | <title>{ title }</title> | ||
| 10 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | 10 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | ||
| 11 | 11 | ||||
| 12 | - | <script src="/assets/js/jquery.min.js"></script> | 12 | + | <script src="/libs/jquery.min.js"></script> |
| 13 | - | <script src="/assets/js/htmx.min.js"></script> | 13 | + | <script src="/libs/htmx.min.js"></script> |
| 14 | - | <script src="/assets/js/htmx-response-targets.js"></script> | 14 | + | <script src="/libs/htmx-response-targets.js"></script> |
| 15 | - | <script src="/assets/js/htmx-debug.js"></script> | 15 | + | <script src="/libs/htmx-debug.js"></script> |
| 16 | - | <script src="/assets/js/htmx-sse.js"></script> | 16 | + | <script src="/libs/htmx-sse.js"></script> |
| 17 | - | <script src="/assets/js/daisyui.js" type="module"></script> | 17 | + | <script src="/libs/daisyui.js" type="module"></script> |
| 18 | - | <script defer src="/assets/js/hyperscript.min.js"></script> | 18 | + | <script defer src="/libs/hyperscript.min.js"></script> |
| 19 | <script defer src="/assets/js/custom.js"></script> | 19 | <script defer src="/assets/js/custom.js"></script> | ||
| 20 | 20 | ||||
| 21 | <!-- daisyUI theme configuration --> | 21 | <!-- daisyUI theme configuration --> | ||
M internal/templating/models/templ/components/header_templ.go +1 -1
| @@ -44,7 +44,7 @@ func Header(title string) templ.Component { | |||
| 44 | 44 | if templ_7745c5c3_Err != nil { | |
| 45 | 45 | return templ_7745c5c3_Err | |
| 46 | 46 | } | |
| 47 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/assets/css/tailwind.css\"><script src=\"/assets/js/jquery.min.js\"></script><script src=\"/assets/js/htmx.min.js\"></script><script src=\"/assets/js/htmx-response-targets.js\"></script><script src=\"/assets/js/htmx-debug.js\"></script><script src=\"/assets/js/htmx-sse.js\"></script><script src=\"/assets/js/daisyui.js\" type=\"module\"></script><script defer src=\"/assets/js/hyperscript.min.js\"></script><script defer src=\"/assets/js/custom.js\"></script><!-- daisyUI theme configuration --><script>\n\t // Check for saved theme preference or use preferred color scheme\n\t const savedTheme = localStorage.getItem('theme') || 'gordon-dark';\n\t document.documentElement.setAttribute('data-theme', savedTheme);\n\t \n\t // Log theme loading for debugging\n\t console.debug('Theme loaded:', savedTheme);\n\t </script></head>") | |
| 47 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/assets/css/tailwind.css\"><script src=\"/libs/jquery.min.js\"></script><script src=\"/libs/htmx.min.js\"></script><script src=\"/libs/htmx-response-targets.js\"></script><script src=\"/libs/htmx-debug.js\"></script><script src=\"/libs/htmx-sse.js\"></script><script src=\"/libs/daisyui.js\" type=\"module\"></script><script defer src=\"/libs/hyperscript.min.js\"></script><script defer src=\"/assets/js/custom.js\"></script><!-- daisyUI theme configuration --><script>\n\t // Check for saved theme preference or use preferred color scheme\n\t const savedTheme = localStorage.getItem('theme') || 'gordon-dark';\n\t document.documentElement.setAttribute('data-theme', savedTheme);\n\t \n\t // Log theme loading for debugging\n\t console.debug('Theme loaded:', savedTheme);\n\t </script></head>") | |
| 48 | 48 | if templ_7745c5c3_Err != nil { | |
| 49 | 49 | return templ_7745c5c3_Err | |
| 50 | 50 | } | |
| @@ -44,7 +44,7 @@ func Header(title string) templ.Component { | |||||
| 44 | if templ_7745c5c3_Err != nil { | 44 | if templ_7745c5c3_Err != nil { | ||
| 45 | return templ_7745c5c3_Err | 45 | return templ_7745c5c3_Err | ||
| 46 | } | 46 | } | ||
| 47 | - | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/assets/css/tailwind.css\"><script src=\"/assets/js/jquery.min.js\"></script><script src=\"/assets/js/htmx.min.js\"></script><script src=\"/assets/js/htmx-response-targets.js\"></script><script src=\"/assets/js/htmx-debug.js\"></script><script src=\"/assets/js/htmx-sse.js\"></script><script src=\"/assets/js/daisyui.js\" type=\"module\"></script><script defer src=\"/assets/js/hyperscript.min.js\"></script><script defer src=\"/assets/js/custom.js\"></script><!-- daisyUI theme configuration --><script>\n\t // Check for saved theme preference or use preferred color scheme\n\t const savedTheme = localStorage.getItem('theme') || 'gordon-dark';\n\t document.documentElement.setAttribute('data-theme', savedTheme);\n\t \n\t // Log theme loading for debugging\n\t console.debug('Theme loaded:', savedTheme);\n\t </script></head>") | 47 | + | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link rel=\"stylesheet\" href=\"/assets/css/tailwind.css\"><script src=\"/libs/jquery.min.js\"></script><script src=\"/libs/htmx.min.js\"></script><script src=\"/libs/htmx-response-targets.js\"></script><script src=\"/libs/htmx-debug.js\"></script><script src=\"/libs/htmx-sse.js\"></script><script src=\"/libs/daisyui.js\" type=\"module\"></script><script defer src=\"/libs/hyperscript.min.js\"></script><script defer src=\"/assets/js/custom.js\"></script><!-- daisyUI theme configuration --><script>\n\t // Check for saved theme preference or use preferred color scheme\n\t const savedTheme = localStorage.getItem('theme') || 'gordon-dark';\n\t document.documentElement.setAttribute('data-theme', savedTheme);\n\t \n\t // Log theme loading for debugging\n\t console.debug('Theme loaded:', savedTheme);\n\t </script></head>") |
| 48 | if templ_7745c5c3_Err != nil { | 48 | if templ_7745c5c3_Err != nil { | ||
| 49 | return templ_7745c5c3_Err | 49 | return templ_7745c5c3_Err | ||
| 50 | } | 50 | } | ||
M internal/templating/models/templ/pages/login/index.templ +4 -4
| @@ -13,13 +13,13 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||
| 13 | 13 | <!-- Custom CSS + Tailwind --> | |
| 14 | 14 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | |
| 15 | 15 | <!-- HTMX min js --> | |
| 16 | - | <script src="/assets/js/htmx.min.js"></script> | |
| 16 | + | <script src="/libs/htmx.min.js"></script> | |
| 17 | 17 | <!-- HTMX SSE --> | |
| 18 | - | <script src="/assets/js/htmx-sse.js"></script> | |
| 18 | + | <script src="/libs/htmx-sse.js"></script> | |
| 19 | 19 | <!-- DaisyUI --> | |
| 20 | - | <script src="/assets/js/daisyui.js" type="module"></script> | |
| 20 | + | <script src="/libs/daisyui.js" type="module"></script> | |
| 21 | 21 | <!-- jQuery --> | |
| 22 | - | <script src="/assets/js/jquery.min.js"></script> | |
| 22 | + | <script src="/libs/jquery.min.js"></script> | |
| 23 | 23 | <!-- Custom JS from assets/js --> | |
| 24 | 24 | <script src="/assets/js/custom.js" defer></script> | |
| 25 | 25 | ||
| @@ -13,13 +13,13 @@ templ LoginPage(title string, buildVersion string, errorMsg string) { | |||||
| 13 | <!-- Custom CSS + Tailwind --> | 13 | <!-- Custom CSS + Tailwind --> | ||
| 14 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | 14 | <link rel="stylesheet" href="/assets/css/tailwind.css" /> | ||
| 15 | <!-- HTMX min js --> | 15 | <!-- HTMX min js --> | ||
| 16 | - | <script src="/assets/js/htmx.min.js"></script> | 16 | + | <script src="/libs/htmx.min.js"></script> |
| 17 | <!-- HTMX SSE --> | 17 | <!-- HTMX SSE --> | ||
| 18 | - | <script src="/assets/js/htmx-sse.js"></script> | 18 | + | <script src="/libs/htmx-sse.js"></script> |
| 19 | <!-- DaisyUI --> | 19 | <!-- DaisyUI --> | ||
| 20 | - | <script src="/assets/js/daisyui.js" type="module"></script> | 20 | + | <script src="/libs/daisyui.js" type="module"></script> |
| 21 | <!-- jQuery --> | 21 | <!-- jQuery --> | ||
| 22 | - | <script src="/assets/js/jquery.min.js"></script> | 22 | + | <script src="/libs/jquery.min.js"></script> |
| 23 | <!-- Custom JS from assets/js --> | 23 | <!-- Custom JS from assets/js --> | ||
| 24 | <script src="/assets/js/custom.js" defer></script> | 24 | <script src="/assets/js/custom.js" defer></script> | ||
| 25 | 25 | ||||
M internal/templating/models/templ/pages/login/index_templ.go +1 -1 generated file: collapsed
A internal/webui/embed.go +9 -0
| @@ -0,0 +1,9 @@ | |||
| 1 | + | package webui | |
| 2 | + | ||
| 3 | + | import "embed" | |
| 4 | + | ||
| 5 | + | // PublicFS holds the embedded static file system for the web UI. | |
| 6 | + | // The path "public" is relative to this package directory (internal/webui). | |
| 7 | + | // | |
| 8 | + | //go:embed public | |
| 9 | + | var PublicFS embed.FS | |
| @@ -0,0 +1,9 @@ | |||||
| 1 | + | package webui | |||
| 2 | + | ||||
| 3 | + | import "embed" | |||
| 4 | + | ||||
| 5 | + | // PublicFS holds the embedded static file system for the web UI. | |||
| 6 | + | // The path "public" is relative to this package directory (internal/webui). | |||
| 7 | + | // | |||
| 8 | + | //go:embed public | |||
| 9 | + | var PublicFS embed.FS | |||