From 12be9facdafa392693264c59e384ba101285c71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Luaces?= Date: Thu, 2 Apr 2026 02:56:47 +0200 Subject: [PATCH] feat(ui): Add customizable OIDC login screen branding (#1583) * feat(ui): Add customizable OIDC login screen branding As per https://github.com/TwiN/gatus/discussions/1579, this PR allows for some customizations in the OIDC login screen: - If a logo is set it will be displayed alongside the Gatus one - New ui config `ui.login-subtitle`: customises the message in the screen - If set, use `ui.header` will be used to customise the title I haven't commited the web static assets. I believe that's triggered with a comment? let me know if I should be doing it. Also please clarify if I've forgotten about something here, this is my first pr. * chore(ui): Regenerate static assets --------- Co-authored-by: github-actions[bot] Co-authored-by: TwiN --- README.md | 5 +++-- config/ui/ui.go | 6 ++++++ config/ui/ui_test.go | 13 +++++++++++++ web/app/public/index.html | 2 +- web/app/src/App.vue | 19 ++++++++++++------- web/static/css/app.css | 2 +- web/static/index.html | 2 +- web/static/js/app.js | 2 +- 8 files changed, 38 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 51dac1c9..ba232dcb 100644 --- a/README.md +++ b/README.md @@ -536,8 +536,8 @@ Allows you to configure the application wide defaults for the dashboard's UI. So | `ui.description` | Meta description for the page. | `Gatus is an advanced...`. | | `ui.dashboard-heading` | Dashboard title between header and endpoints | `Health Dashboard` | | `ui.dashboard-subheading` | Dashboard description between header and endpoints | `Monitor the health of your endpoints in real-time` | -| `ui.header` | Header at the top of the dashboard. | `Gatus` | -| `ui.logo` | URL to the logo to display. | `""` | +| `ui.header` | Header at the top of the dashboard. Also used as the title on the OIDC login page. | `Gatus` | +| `ui.logo` | URL to the logo to display. When set, shown alongside the Gatus logo on the OIDC login page. | `""` | | `ui.link` | Link to open when the logo is clicked. | `""` | | `ui.favicon.default` | Favourite default icon to display in web browser tab or address bar. | `/favicon.ico` | | `ui.favicon.size16x16` | Favourite icon to display in web browser for 16x16 size. | `/favicon-16x16.png` | @@ -549,6 +549,7 @@ Allows you to configure the application wide defaults for the dashboard's UI. So | `ui.dark-mode` | Whether to enable dark mode by default. Note that this is superseded by the user's operating system theme preferences. | `true` | | `ui.default-sort-by` | Default sorting option for endpoints in the dashboard. Can be `name`, `group`, or `health`. Note that user preferences override this. | `name` | | `ui.default-filter-by` | Default filter option for endpoints in the dashboard. Can be `none`, `failing`, or `unstable`. Note that user preferences override this. | `none` | +| `ui.login-subtitle` | Subtitle displayed on the OIDC login page. | `System Monitoring Dashboard` | ### Announcements System-wide announcements allow you to display important messages at the top of the status page. These can be used to inform users about planned maintenance, ongoing issues, or general information. You can use markdown to format your announcements. diff --git a/config/ui/ui.go b/config/ui/ui.go index 6a62a33f..0feaeb9b 100644 --- a/config/ui/ui.go +++ b/config/ui/ui.go @@ -23,6 +23,7 @@ const ( defaultCustomCSS = "" defaultSortBy = "name" defaultFilterBy = "none" + defaultLoginSubtitle = "System Monitoring Dashboard" ) var ( @@ -48,6 +49,7 @@ type Config struct { DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health') DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable') + LoginSubtitle string `yaml:"login-subtitle,omitempty"` // LoginSubtitle is the subtitle displayed on the OIDC login page ////////////////////////////////////////////// // Non-configurable - used for UI rendering // ////////////////////////////////////////////// @@ -95,6 +97,7 @@ func GetDefaultConfig() *Config { DarkMode: &defaultDarkMode, DefaultSortBy: defaultSortBy, DefaultFilterBy: defaultFilterBy, + LoginSubtitle: defaultLoginSubtitle, MaximumNumberOfResults: storage.DefaultMaximumNumberOfResults, Favicon: Favicon{ Default: defaultFavicon, @@ -143,6 +146,9 @@ func (cfg *Config) ValidateAndSetDefaults() error { } else if cfg.DefaultFilterBy != "none" && cfg.DefaultFilterBy != "failing" && cfg.DefaultFilterBy != "unstable" { return ErrInvalidDefaultFilterBy } + if len(cfg.LoginSubtitle) == 0 { + cfg.LoginSubtitle = defaultLoginSubtitle + } if len(cfg.Favicon.Default) == 0 { cfg.Favicon.Default = defaultFavicon } diff --git a/config/ui/ui_test.go b/config/ui/ui_test.go index 536872ef..6e1ab130 100644 --- a/config/ui/ui_test.go +++ b/config/ui/ui_test.go @@ -50,6 +50,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) { if cfg.Favicon.Size32x32 != defaultFavicon32 { t.Errorf("expected favicon to be %s, got %s", defaultFavicon32, cfg.Favicon.Size32x32) } + if cfg.LoginSubtitle != defaultLoginSubtitle { + t.Errorf("expected LoginSubtitle to be %s, got %s", defaultLoginSubtitle, cfg.LoginSubtitle) + } }) t.Run("custom-values", func(t *testing.T) { cfg := &Config{ @@ -62,6 +65,7 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) { Link: "https://example.com", DefaultSortBy: "health", DefaultFilterBy: "failing", + LoginSubtitle: "Welcome", } if err := cfg.ValidateAndSetDefaults(); err != nil { t.Error("expected no error, got", err.Error()) @@ -93,6 +97,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) { if cfg.DefaultFilterBy != "failing" { t.Errorf("expected defaultFilterBy to be preserved, got %s", cfg.DefaultFilterBy) } + if cfg.LoginSubtitle != "Welcome" { + t.Errorf("expected LoginSubtitle to be preserved, got %s", cfg.LoginSubtitle) + } }) t.Run("partial-custom-values", func(t *testing.T) { cfg := &Config{ @@ -119,6 +126,9 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) { if cfg.Description != defaultDescription { t.Errorf("expected description to use default, got %s", cfg.Description) } + if cfg.LoginSubtitle != defaultLoginSubtitle { + t.Errorf("expected LoginSubtitle to use default, got %s", cfg.LoginSubtitle) + } }) } @@ -181,6 +191,9 @@ func TestGetDefaultConfig(t *testing.T) { if defaultConfig.DefaultFilterBy != defaultFilterBy { t.Error("expected GetDefaultConfig() to return defaultFilterBy, got", defaultConfig.DefaultFilterBy) } + if defaultConfig.LoginSubtitle != defaultLoginSubtitle { + t.Error("expected GetDefaultConfig() to return defaultLoginSubtitle, got", defaultConfig.LoginSubtitle) + } } func TestConfig_ValidateAndSetDefaults_DefaultSortBy(t *testing.T) { diff --git a/web/app/public/index.html b/web/app/public/index.html index 890fdf1f..cdeeb435 100644 --- a/web/app/public/index.html +++ b/web/app/public/index.html @@ -3,7 +3,7 @@