diff --git a/README.md b/README.md index d3c8512..6f7d915 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ handler := monitor.New(mux, monitor.Config{ Title: "My App Monitor", Description: "Live production service metrics.", Footer: "Copyright 2026 Example Inc.", + FaviconURL: "/assets/favicon.svg", DefaultLanguage: "en", DefaultTheme: "dark", Background: "solid", @@ -270,6 +271,7 @@ Defaults: | `Title` | `Monitor` | HTML page title and heading. | | `Description` | `Live process, runtime, system, and HTTP metrics for this Go service.` | Short visible description below the header. | | `Footer` | `Powered by github.com/gofurry/monitor - MIT License.` | Footer text for copyright, ownership, or license notes. | +| `FaviconURL` | built-in favicon | Overrides the dashboard favicon with a root-relative path or absolute HTTP(S) URL. Empty or invalid values use the built-in favicon. | | `DefaultLanguage` | `en` | Initial UI language when no browser preference is saved. Supported values: `en`, `zh-CN`. | | `DefaultTheme` | `dark` | Initial UI theme when no browser preference is saved. Supported values: `light`, `dark`. | | `Background` | `solid` | HTML page background. Supported values: `solid`, `grid`. | @@ -281,6 +283,18 @@ Defaults: Requests to `Path` are always excluded from `http.total_requests`; the monitor page and its JSON polling do not inflate the business request count. `IgnoreRequest` is for other non-business traffic, such as load balancer probes or health checks. Ignored requests are still served by your handler. +### Dashboard favicon + +The dashboard includes an embedded favicon by default. Set `FaviconURL` to use a favicon served by your application or a remote HTTP(S) URL: + +```go +handler := monitor.New(mux, monitor.Config{ + FaviconURL: "/assets/favicon.svg", +}) +``` + +Filesystem paths such as `./favicon.ico` are not supported directly. Serve the file through your application first, then configure its URL. A same-origin URL is preferred because a remote favicon causes every dashboard visitor's browser to contact that host. + ## Best Practice `monitor` does not persist metrics, logs, traces, or chart history. It shows the current process, current host, Go runtime, and requests handled by this middleware instance. diff --git a/config.go b/config.go index 4b74b09..ce1d2c6 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,8 @@ package monitor import ( "net/http" + "net/url" + "strings" "time" ) @@ -37,6 +39,11 @@ type Config struct { // copyright, ownership, or license text. Footer string + // FaviconURL overrides the built-in dashboard favicon. It supports a + // root-relative path or an absolute HTTP(S) URL. Empty or invalid values use + // the built-in favicon. + FaviconURL string + // DefaultLanguage controls the initial HTML UI language when the browser has // no saved monitor language preference. Supported values are "en" and // "zh-CN". Empty or unsupported values use "en". @@ -109,6 +116,7 @@ func applyConfig(configs []Config) Config { if cfg.Footer == "" { cfg.Footer = defaultFooter } + cfg.FaviconURL = normalizeFaviconURL(cfg.FaviconURL) if !isSupportedLanguage(cfg.DefaultLanguage) { cfg.DefaultLanguage = defaultLanguage } @@ -130,6 +138,25 @@ func applyConfig(configs []Config) Config { return cfg } +func normalizeFaviconURL(rawURL string) string { + rawURL = strings.TrimSpace(rawURL) + if rawURL == "" { + return "" + } + + parsed, err := url.Parse(rawURL) + if err != nil { + return "" + } + if strings.HasPrefix(rawURL, "/") && !strings.HasPrefix(rawURL, "//") && !strings.HasPrefix(rawURL, "/\\") && parsed.Scheme == "" && parsed.Host == "" { + return rawURL + } + if (strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")) && parsed.Host != "" && parsed.User == nil { + return rawURL + } + return "" +} + func isSupportedLanguage(lang string) bool { return lang == "en" || lang == "zh-CN" } diff --git a/docs/releases/v1.1.1.md b/docs/releases/v1.1.1.md new file mode 100644 index 0000000..975b637 --- /dev/null +++ b/docs/releases/v1.1.1.md @@ -0,0 +1,4 @@ +# v1.1.1 Release Notes + +- Add a built-in dashboard favicon with `Config.FaviconURL` support for root-relative and HTTP(S) URLs. +- Invalid or empty favicon URLs safely fall back to the built-in icon. diff --git a/html.go b/html.go index 6913580..eabc588 100644 --- a/html.go +++ b/html.go @@ -3,11 +3,19 @@ package monitor import ( "bytes" _ "embed" + "encoding/base64" "encoding/json" "html/template" "time" ) +const defaultMonitorFaviconSVG = `` + +var defaultMonitorFaviconURL = "data:image/svg+xml;base64," + base64.StdEncoding.EncodeToString([]byte(defaultMonitorFaviconSVG)) + //go:embed internal/ui/page.html var monitorPageHTML string @@ -23,6 +31,7 @@ type monitorPageData struct { Title string Description string Footer string + FaviconURL any CSS template.CSS JS template.JS ConfigJSON template.JS @@ -41,6 +50,11 @@ type monitorClientConfig struct { } func renderHTML(cfg Config) string { + var faviconURL any = cfg.FaviconURL + if cfg.FaviconURL == "" { + faviconURL = template.URL(defaultMonitorFaviconURL) // #nosec G203 -- this is the package-owned embedded favicon. + } + refreshMS := maxInt64(int64(cfg.Refresh/time.Millisecond), 250) configJSON, _ := json.Marshal(monitorClientConfig{ RefreshMS: refreshMS, @@ -53,6 +67,7 @@ func renderHTML(cfg Config) string { Title: cfg.Title, Description: cfg.Description, Footer: cfg.Footer, + FaviconURL: faviconURL, CSS: template.CSS(monitorStyleCSS), JS: template.JS(monitorAppJS), ConfigJSON: template.JS(configJSON), diff --git a/internal/ui/page.html b/internal/ui/page.html index 7d32ae0..af68948 100644 --- a/internal/ui/page.html +++ b/internal/ui/page.html @@ -4,6 +4,7 @@ +