From 333d40ed4f45b3037c72fd13fb8c7c6a4c8644f6 Mon Sep 17 00:00:00 2001 From: David Pearson Date: Sun, 20 Apr 2025 10:00:17 -0400 Subject: [PATCH 1/5] Dynamize manifest.json with configurable options --- docs/configuration.md | 15 ++++++++++++ internal/glance/config.go | 3 +++ internal/glance/glance.go | 32 +++++++++++++++++++++++++ internal/glance/static/manifest.json | 14 ----------- internal/glance/templates/manifest.json | 14 +++++++++++ 5 files changed, 64 insertions(+), 14 deletions(-) delete mode 100644 internal/glance/static/manifest.json create mode 100644 internal/glance/templates/manifest.json diff --git a/docs/configuration.md b/docs/configuration.md index adb73c4..8ebcb0e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -268,6 +268,9 @@ branding:

Powered by Glance

logo-url: /assets/logo.png favicon-url: /assets/logo.png + app-name: "My Dashboard" + app-icon-url: "/assets/app-icon.png" + app-bg-color: "#151519" ``` ### Properties @@ -279,6 +282,9 @@ branding: | logo-text | string | no | G | | logo-url | string | no | | | favicon-url | string | no | | +| app-name | string | no | Glance | +| app-icon-url | string | no | | +| app-bg-color | string | no | #151519 | #### `hide-footer` Hides the footer when set to `true`. @@ -295,6 +301,15 @@ Specify a URL to a custom image to use instead of the "G" found in the navigatio #### `favicon-url` Specify a URL to a custom image to use for the favicon. +#### `app-name` +Specify the name of the web app shown in browser tab and PWA. Defaults to "Glance". + +#### `app-icon-url` +Specify URL for PWA and browser tab icon (512x512 PNG). Defaults to Glance icon if not set. + +#### `app-bg-color` +Specify background color for PWA. Must be a valid CSS color. Defaults to "#151519". + ## Theme Theming is done through a top level `theme` property. Values for the colors are in [HSL](https://giggster.com/guide/basics/hue-saturation-lightness/) (hue, saturation, lightness) format. You can use a color picker [like this one](https://hslpicker.com/) to convert colors from other formats to HSL. The values are separated by a space and `%` is not required for any of the numbers. diff --git a/internal/glance/config.go b/internal/glance/config.go index a63afff..4b9060c 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -55,6 +55,9 @@ type config struct { LogoText string `yaml:"logo-text"` LogoURL string `yaml:"logo-url"` FaviconURL string `yaml:"favicon-url"` + AppName string `yaml:"app-name"` + AppIconURL string `yaml:"app-icon-url"` + AppBgColor string `yaml:"app-bg-color"` } `yaml:"branding"` Pages []page `yaml:"pages"` diff --git a/internal/glance/glance.go b/internal/glance/glance.go index ab63536..7952de1 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -96,6 +96,18 @@ func newApplication(config *config) (*application, error) { config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL) } + if config.Branding.AppName == "" { + config.Branding.AppName = "Glance" + } + + if config.Branding.AppIconURL == "" { + config.Branding.AppIconURL = app.AssetPath("app-icon.png") + } + + if config.Branding.AppBgColor == "" { + config.Branding.AppBgColor = "#151519" + } + config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) return app, nil @@ -257,6 +269,26 @@ func (a *application) server() (func() error, func() error) { w.Write(bundledCSSContents) }) + mux.HandleFunc(fmt.Sprintf("GET /static/%s/manifest.json", staticFSHash), func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-Control", cssBundleCacheControlValue) + w.Header().Add("Content-Type", "application/json") + + template, err := template.New("manifest.json"). + Funcs(globalTemplateFunctions). + ParseFS(templateFS, "manifest.json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Error parsing manifest.json template: %v", err))) + return + } + + if err := template.Execute(w, pageTemplateData{App: a}); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("Error executing manifest.json template: %v", err))) + return + } + }) + var absAssetsPath string if a.Config.Server.AssetsPath != "" { absAssetsPath, _ = filepath.Abs(a.Config.Server.AssetsPath) diff --git a/internal/glance/static/manifest.json b/internal/glance/static/manifest.json deleted file mode 100644 index 42e8213..0000000 --- a/internal/glance/static/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Glance", - "display": "standalone", - "background_color": "#151519", - "scope": "/", - "start_url": "/", - "icons": [ - { - "src": "app-icon.png", - "type": "image/png", - "sizes": "512x512" - } - ] -} diff --git a/internal/glance/templates/manifest.json b/internal/glance/templates/manifest.json new file mode 100644 index 0000000..a7ccce4 --- /dev/null +++ b/internal/glance/templates/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "{{ .App.Config.Branding.AppName }}", + "display": "standalone", + "background_color": "{{ .App.Config.Branding.AppBgColor }}", + "scope": "/", + "start_url": "/", + "icons": [ + { + "src": "{{ .App.Config.Branding.AppIconURL }}", + "type": "image/png", + "sizes": "512x512" + } + ] +} \ No newline at end of file From e2112e0d8313226415e7a7abf892b9674c230148 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:55:58 +0100 Subject: [PATCH 2/5] Change HSL values to floats --- internal/glance/config-fields.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/glance/config-fields.go b/internal/glance/config-fields.go index cd244e5..f3a1649 100644 --- a/internal/glance/config-fields.go +++ b/internal/glance/config-fields.go @@ -14,7 +14,7 @@ import ( "gopkg.in/yaml.v3" ) -var hslColorFieldPattern = regexp.MustCompile(`^(?:hsla?\()?(\d{1,3})(?: |,)+(\d{1,3})%?(?: |,)+(\d{1,3})%?\)?$`) +var hslColorFieldPattern = regexp.MustCompile(`^(?:hsla?\()?([\d\.]+)(?: |,)+([\d\.]+)%?(?: |,)+([\d\.]+)%?\)?$`) const ( hslHueMax = 360 @@ -23,13 +23,13 @@ const ( ) type hslColorField struct { - Hue uint16 - Saturation uint8 - Lightness uint8 + Hue float64 + Saturation float64 + Lightness float64 } func (c *hslColorField) String() string { - return fmt.Sprintf("hsl(%d, %d%%, %d%%)", c.Hue, c.Saturation, c.Lightness) + return fmt.Sprintf("hsl(%.1f, %.1f%%, %.1f%%)", c.Hue, c.Saturation, c.Lightness) } func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { @@ -45,7 +45,7 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("invalid HSL color format: %s", value) } - hue, err := strconv.ParseUint(matches[1], 10, 16) + hue, err := strconv.ParseFloat(matches[1], 64) if err != nil { return err } @@ -54,7 +54,7 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("HSL hue must be between 0 and %d", hslHueMax) } - saturation, err := strconv.ParseUint(matches[2], 10, 8) + saturation, err := strconv.ParseFloat(matches[2], 64) if err != nil { return err } @@ -63,7 +63,7 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("HSL saturation must be between 0 and %d", hslSaturationMax) } - lightness, err := strconv.ParseUint(matches[3], 10, 8) + lightness, err := strconv.ParseFloat(matches[3], 64) if err != nil { return err } @@ -72,9 +72,9 @@ func (c *hslColorField) UnmarshalYAML(node *yaml.Node) error { return fmt.Errorf("HSL lightness must be between 0 and %d", hslLightnessMax) } - c.Hue = uint16(hue) - c.Saturation = uint8(saturation) - c.Lightness = uint8(lightness) + c.Hue = hue + c.Saturation = saturation + c.Lightness = lightness return nil } From 4e4c3cfe64218a7efc58caa299fafaf337719d99 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:09:33 +0100 Subject: [PATCH 3/5] Update dynamic manifest implementation --- docs/configuration.md | 14 +++--- internal/glance/config.go | 25 +++++------ internal/glance/glance.go | 58 +++++++++++++------------ internal/glance/templates/document.html | 8 ++-- internal/glance/templates/manifest.json | 4 +- internal/glance/templates/page.html | 2 +- 6 files changed, 57 insertions(+), 54 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8ebcb0e..977a890 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -270,7 +270,7 @@ branding: favicon-url: /assets/logo.png app-name: "My Dashboard" app-icon-url: "/assets/app-icon.png" - app-bg-color: "#151519" + app-background-color: "#151519" ``` ### Properties @@ -283,8 +283,8 @@ branding: | logo-url | string | no | | | favicon-url | string | no | | | app-name | string | no | Glance | -| app-icon-url | string | no | | -| app-bg-color | string | no | #151519 | +| app-icon-url | string | no | Glance's default icon | +| app-background-color | string | no | Glance's default background color | #### `hide-footer` Hides the footer when set to `true`. @@ -302,13 +302,13 @@ Specify a URL to a custom image to use instead of the "G" found in the navigatio Specify a URL to a custom image to use for the favicon. #### `app-name` -Specify the name of the web app shown in browser tab and PWA. Defaults to "Glance". +Specify the name of the web app shown in browser tab and PWA. #### `app-icon-url` -Specify URL for PWA and browser tab icon (512x512 PNG). Defaults to Glance icon if not set. +Specify URL for PWA and browser tab icon (512x512 PNG). -#### `app-bg-color` -Specify background color for PWA. Must be a valid CSS color. Defaults to "#151519". +#### `app-background-color` +Specify background color for PWA. Must be a valid CSS color. ## Theme Theming is done through a top level `theme` property. Values for the colors are in [HSL](https://giggster.com/guide/basics/hue-saturation-lightness/) (hue, saturation, lightness) format. You can use a color picker [like this one](https://hslpicker.com/) to convert colors from other formats to HSL. The values are separated by a space and `%` is not required for any of the numbers. diff --git a/internal/glance/config.go b/internal/glance/config.go index 4b9060c..2053ff9 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -27,11 +27,10 @@ const ( type config struct { Server struct { - Host string `yaml:"host"` - Port uint16 `yaml:"port"` - AssetsPath string `yaml:"assets-path"` - BaseURL string `yaml:"base-url"` - StartedAt time.Time `yaml:"-"` // used in custom css file + Host string `yaml:"host"` + Port uint16 `yaml:"port"` + AssetsPath string `yaml:"assets-path"` + BaseURL string `yaml:"base-url"` } `yaml:"server"` Document struct { @@ -50,14 +49,14 @@ type config struct { } `yaml:"theme"` Branding struct { - HideFooter bool `yaml:"hide-footer"` - CustomFooter template.HTML `yaml:"custom-footer"` - LogoText string `yaml:"logo-text"` - LogoURL string `yaml:"logo-url"` - FaviconURL string `yaml:"favicon-url"` - AppName string `yaml:"app-name"` - AppIconURL string `yaml:"app-icon-url"` - AppBgColor string `yaml:"app-bg-color"` + HideFooter bool `yaml:"hide-footer"` + CustomFooter template.HTML `yaml:"custom-footer"` + LogoText string `yaml:"logo-text"` + LogoURL string `yaml:"logo-url"` + FaviconURL string `yaml:"favicon-url"` + AppName string `yaml:"app-name"` + AppIconURL string `yaml:"app-icon-url"` + AppBackgroundColor string `yaml:"app-background-color"` } `yaml:"branding"` Pages []page `yaml:"pages"` diff --git a/internal/glance/glance.go b/internal/glance/glance.go index 7952de1..e000f06 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -18,15 +18,19 @@ var ( pageTemplate = mustParseTemplate("page.html", "document.html") pageContentTemplate = mustParseTemplate("page-content.html") pageThemeStyleTemplate = mustParseTemplate("theme-style.gotmpl") + manifestTemplate = mustParseTemplate("manifest.json") ) const STATIC_ASSETS_CACHE_DURATION = 24 * time.Hour type application struct { Version string + CreatedAt time.Time Config config ParsedThemeStyle template.HTML + parsedManifest []byte + slugToPage map[string]*page widgetByID map[uint64]widget } @@ -34,6 +38,7 @@ type application struct { func newApplication(config *config) (*application, error) { app := &application{ Version: buildVersion, + CreatedAt: time.Now(), Config: *config, slugToPage: make(map[string]*page), widgetByID: make(map[uint64]widget), @@ -42,7 +47,7 @@ func newApplication(config *config) (*application, error) { app.slugToPage[""] = &config.Pages[0] providers := &widgetProviders{ - assetResolver: app.AssetPath, + assetResolver: app.StaticAssetPath, } var err error @@ -89,9 +94,10 @@ func newApplication(config *config) (*application, error) { config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/") config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile) + config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) if config.Branding.FaviconURL == "" { - config.Branding.FaviconURL = app.AssetPath("favicon.png") + config.Branding.FaviconURL = app.StaticAssetPath("favicon.png") } else { config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL) } @@ -101,14 +107,22 @@ func newApplication(config *config) (*application, error) { } if config.Branding.AppIconURL == "" { - config.Branding.AppIconURL = app.AssetPath("app-icon.png") + config.Branding.AppIconURL = app.StaticAssetPath("app-icon.png") } - if config.Branding.AppBgColor == "" { - config.Branding.AppBgColor = "#151519" + if config.Branding.AppBackgroundColor == "" { + config.Branding.AppBackgroundColor = ternary( + config.Theme.BackgroundColor != nil, + config.Theme.BackgroundColor.String(), + "hsl(240, 8%, 9%)", + ) } - config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) + var manifestWriter bytes.Buffer + if err := manifestTemplate.Execute(&manifestWriter, pageTemplateData{App: app}); err != nil { + return nil, fmt.Errorf("parsing manifest.json: %v", err) + } + app.parsedManifest = manifestWriter.Bytes() return app, nil } @@ -232,10 +246,15 @@ func (a *application) handleWidgetRequest(w http.ResponseWriter, r *http.Request widget.handleRequest(w, r) } -func (a *application) AssetPath(asset string) string { +func (a *application) StaticAssetPath(asset string) string { return a.Config.Server.BaseURL + "/static/" + staticFSHash + "/" + asset } +func (a *application) VersionedAssetPath(asset string) string { + return a.Config.Server.BaseURL + asset + + "?v=" + strconv.FormatInt(a.CreatedAt.Unix(), 10) +} + func (a *application) server() (func() error, func() error) { // TODO: add gzip support, static files must have their gzipped contents cached // TODO: add HTTPS support @@ -258,35 +277,21 @@ func (a *application) server() (func() error, func() error) { ), ) - cssBundleCacheControlValue := fmt.Sprintf( + assetCacheControlValue := fmt.Sprintf( "public, max-age=%d", int(STATIC_ASSETS_CACHE_DURATION.Seconds()), ) mux.HandleFunc(fmt.Sprintf("GET /static/%s/css/bundle.css", staticFSHash), func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", cssBundleCacheControlValue) + w.Header().Add("Cache-Control", assetCacheControlValue) w.Header().Add("Content-Type", "text/css; charset=utf-8") w.Write(bundledCSSContents) }) - mux.HandleFunc(fmt.Sprintf("GET /static/%s/manifest.json", staticFSHash), func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", cssBundleCacheControlValue) + mux.HandleFunc("GET /manifest.json", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-Control", assetCacheControlValue) w.Header().Add("Content-Type", "application/json") - - template, err := template.New("manifest.json"). - Funcs(globalTemplateFunctions). - ParseFS(templateFS, "manifest.json") - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Error parsing manifest.json template: %v", err))) - return - } - - if err := template.Execute(w, pageTemplateData{App: a}); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("Error executing manifest.json template: %v", err))) - return - } + w.Write(a.parsedManifest) }) var absAssetsPath string @@ -302,7 +307,6 @@ func (a *application) server() (func() error, func() error) { } start := func() error { - a.Config.Server.StartedAt = time.Now() log.Printf("Starting server on %s:%d (base-url: \"%s\", assets-path: \"%s\")\n", a.Config.Server.Host, a.Config.Server.Port, diff --git a/internal/glance/templates/document.html b/internal/glance/templates/document.html index a6896e0..107a749 100644 --- a/internal/glance/templates/document.html +++ b/internal/glance/templates/document.html @@ -12,11 +12,11 @@ - - + + - - + + {{ block "document-head-after" . }}{{ end }} diff --git a/internal/glance/templates/manifest.json b/internal/glance/templates/manifest.json index a7ccce4..eae4d19 100644 --- a/internal/glance/templates/manifest.json +++ b/internal/glance/templates/manifest.json @@ -1,7 +1,7 @@ { "name": "{{ .App.Config.Branding.AppName }}", "display": "standalone", - "background_color": "{{ .App.Config.Branding.AppBgColor }}", + "background_color": "{{ .App.Config.Branding.AppBackgroundColor }}", "scope": "/", "start_url": "/", "icons": [ @@ -11,4 +11,4 @@ "sizes": "512x512" } ] -} \ No newline at end of file +} diff --git a/internal/glance/templates/page.html b/internal/glance/templates/page.html index 8e1ddae..24baf78 100644 --- a/internal/glance/templates/page.html +++ b/internal/glance/templates/page.html @@ -17,7 +17,7 @@ {{ .App.ParsedThemeStyle }} {{ if ne "" .App.Config.Theme.CustomCSSFile }} - + {{ end }} {{ if ne "" .App.Config.Document.Head }}{{ .App.Config.Document.Head }}{{ end }} From 689e8f216c23940ca490198621b7e815a5724c08 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:10:15 +0100 Subject: [PATCH 4/5] Rename function --- internal/glance/glance.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/glance/glance.go b/internal/glance/glance.go index e000f06..903cfbc 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -93,13 +93,13 @@ func newApplication(config *config) (*application, error) { config = &app.Config config.Server.BaseURL = strings.TrimRight(config.Server.BaseURL, "/") - config.Theme.CustomCSSFile = app.transformUserDefinedAssetPath(config.Theme.CustomCSSFile) - config.Branding.LogoURL = app.transformUserDefinedAssetPath(config.Branding.LogoURL) + config.Theme.CustomCSSFile = app.resolveUserDefinedAssetPath(config.Theme.CustomCSSFile) + config.Branding.LogoURL = app.resolveUserDefinedAssetPath(config.Branding.LogoURL) if config.Branding.FaviconURL == "" { config.Branding.FaviconURL = app.StaticAssetPath("favicon.png") } else { - config.Branding.FaviconURL = app.transformUserDefinedAssetPath(config.Branding.FaviconURL) + config.Branding.FaviconURL = app.resolveUserDefinedAssetPath(config.Branding.FaviconURL) } if config.Branding.AppName == "" { @@ -152,7 +152,7 @@ func (p *page) updateOutdatedWidgets() { wg.Wait() } -func (a *application) transformUserDefinedAssetPath(path string) string { +func (a *application) resolveUserDefinedAssetPath(path string) string { if strings.HasPrefix(path, "/assets/") { return a.Config.Server.BaseURL + path } From 93c72f561b92ad4f1e7affaa48050db52dd079c5 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 23 Apr 2025 12:18:46 +0100 Subject: [PATCH 5/5] Also use app properties for apple-related elements --- internal/glance/templates/document.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/glance/templates/document.html b/internal/glance/templates/document.html index 107a749..26beab9 100644 --- a/internal/glance/templates/document.html +++ b/internal/glance/templates/document.html @@ -10,9 +10,9 @@ - + - +