From 0c8358beaa95730caa9deffd60db67d10793ad0e Mon Sep 17 00:00:00 2001 From: Kevin <15876989+KevinFumbles@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:58:05 -0500 Subject: [PATCH 001/142] Added Technitium as a valid service for dns-stats widget --- internal/glance/widget-dns-stats.go | 152 +++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index 833a80d..6ba506d 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -48,7 +48,11 @@ func (widget *dnsStatsWidget) initialize() error { withTitleURL(string(widget.URL)). withCacheDuration(10 * time.Minute) - if widget.Service != "adguard" && widget.Service != "pihole" { + switch widget.Service { + case "adguard": + case "pihole": + case "technitium": + default: return errors.New("service must be either 'adguard' or 'pihole'") } @@ -59,10 +63,13 @@ func (widget *dnsStatsWidget) update(ctx context.Context) { var stats *dnsStats var err error - if widget.Service == "adguard" { + switch widget.Service { + case "adguard": stats, err = fetchAdguardStats(widget.URL, widget.AllowInsecure, widget.Username, widget.Password, widget.HideGraph) - } else { + case "pihole": stats, err = fetchPiholeStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph) + case "technitium": + stats, err = fetchTechnitiumStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph) } if !widget.canContinueUpdateAfterHandlingErr(err) { @@ -379,3 +386,142 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr return stats, nil } + +type technitiumStatsResponse struct { + Response struct { + Stats struct { + TotalQueries int `json:"totalQueries"` + BlockedQueries int `json:"totalBlocked"` + } `json:"stats"` + MainChartData struct { + Datasets []struct { + Label string `json:"label"` + Data []int `json:"data"` + } `json:"datasets"` + } `json:"mainChartData"` + TopBlockedDomains []struct { + Domain string `json:"name"` + Count int `json:"hits"` + } + } `json:"response"` +} + +func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) { + + if token == "" { + return nil, errors.New("missing API token") + } + + requestURL := strings.TrimRight(instanceUrl, "/") + "/api/dashboard/stats/get?token=" + token + "&type=LastDay" + + request, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + + var client requestDoer + if !allowInsecure { + client = defaultHTTPClient + } else { + client = defaultInsecureHTTPClient + } + + responseJson, err := decodeJsonFromRequest[technitiumStatsResponse](client, request) + if err != nil { + return nil, err + } + + var topBlockedDomainsCount = min(len(responseJson.Response.TopBlockedDomains), 5) + + stats := &dnsStats{ + TotalQueries: responseJson.Response.Stats.TotalQueries, + BlockedQueries: responseJson.Response.Stats.BlockedQueries, + TopBlockedDomains: make([]dnsStatsBlockedDomain, 0, topBlockedDomainsCount), + } + + if stats.TotalQueries <= 0 { + return stats, nil + } + + stats.BlockedPercent = int(float64(responseJson.Response.Stats.BlockedQueries) / float64(responseJson.Response.Stats.TotalQueries) * 100) + + for i := 0; i < topBlockedDomainsCount; i++ { + domain := responseJson.Response.TopBlockedDomains[i] + firstDomain := domain.Domain + + if firstDomain == "" { + continue + } + + stats.TopBlockedDomains = append(stats.TopBlockedDomains, dnsStatsBlockedDomain{ + Domain: firstDomain, + }) + + if stats.BlockedQueries > 0 { + stats.TopBlockedDomains[i].PercentBlocked = int(float64(domain.Count) / float64(responseJson.Response.Stats.BlockedQueries) * 100) + } + } + + if noGraph { + return stats, nil + } + + var queriesSeries, blockedSeries []int + + for _, label := range responseJson.Response.MainChartData.Datasets { + switch label.Label { + case "Total": + queriesSeries = label.Data + case "Blocked": + blockedSeries = label.Data + } + } + + const bars = 8 + const hoursSpan = 24 + const hoursPerBar int = hoursSpan / bars + + if len(queriesSeries) > hoursSpan { + queriesSeries = queriesSeries[len(queriesSeries)-hoursSpan:] + } else if len(queriesSeries) < hoursSpan { + queriesSeries = append(make([]int, hoursSpan-len(queriesSeries)), queriesSeries...) + } + + if len(blockedSeries) > hoursSpan { + blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:] + } else if len(blockedSeries) < hoursSpan { + blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...) + } + + maxQueriesInSeries := 0 + + for i := 0; i < bars; i++ { + queries := 0 + blocked := 0 + + for j := 0; j < hoursPerBar; j++ { + queries += queriesSeries[i*hoursPerBar+j] + blocked += blockedSeries[i*hoursPerBar+j] + } + + stats.Series[i] = dnsStatsSeries{ + Queries: queries, + Blocked: blocked, + } + + if queries > 0 { + stats.Series[i].PercentBlocked = int(float64(blocked) / float64(queries) * 100) + } + + if queries > maxQueriesInSeries { + maxQueriesInSeries = queries + } + } + + for i := 0; i < bars; i++ { + stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) + } + + return stats, nil + +} From baee94ed1de8ef43413f93553177be18bc276722 Mon Sep 17 00:00:00 2001 From: Kevin <15876989+KevinFumbles@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:58:21 -0500 Subject: [PATCH 002/142] Added configuration documentation for Technitium dns-stats service --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0cee1ec..a1f3523 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1737,7 +1737,7 @@ The path to the Docker socket. | glance.parent | The ID of the parent container. Used to group containers under a single parent. | ### DNS Stats -Display statistics from a self-hosted ad-blocking DNS resolver such as AdGuard Home or Pi-hole. +Display statistics from a self-hosted ad-blocking DNS resolver such as AdGuard Home, Pi-hole, or Technitium. Example: @@ -1755,7 +1755,7 @@ Preview: > [!NOTE] > -> When using AdGuard Home the 3rd statistic on top will be the average latency and when using Pi-hole it will be the total number of blocked domains from all adlists. +> When using AdGuard Home the 3rd statistic on top will be the average latency and when using Pi-hole or Technitium it will be the total number of blocked domains from all adlists. #### Properties @@ -1772,7 +1772,7 @@ Preview: | hour-format | string | no | 12h | ##### `service` -Either `adguard` or `pihole`. +Either `adguard`, `pihole`, or `technitium`. ##### `allow-insecure` Whether to allow invalid/self-signed certificates when making the request to the service. @@ -1787,7 +1787,7 @@ Only required when using AdGuard Home. The username used to log into the admin d Only required when using AdGuard Home. The password used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. ##### `token` -Only required when using Pi-hole. The API token which can be found in `Settings -> API -> Show API token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. ##### `hide-graph` Whether to hide the graph showing the number of queries over time. From 94806ed45dbdb15c32b9792980344a94e320d9ff Mon Sep 17 00:00:00 2001 From: Kevin <15876989+KevinFumbles@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:05:38 -0500 Subject: [PATCH 003/142] Added blocked domains count for Technitium --- internal/glance/widget-dns-stats.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index 6ba506d..a137228 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -392,6 +392,8 @@ type technitiumStatsResponse struct { Stats struct { TotalQueries int `json:"totalQueries"` BlockedQueries int `json:"totalBlocked"` + BlockedZones int `json:"blockedZones"` + BlockListZones int `json:"blockListZones"` } `json:"stats"` MainChartData struct { Datasets []struct { @@ -437,6 +439,7 @@ func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, TotalQueries: responseJson.Response.Stats.TotalQueries, BlockedQueries: responseJson.Response.Stats.BlockedQueries, TopBlockedDomains: make([]dnsStatsBlockedDomain, 0, topBlockedDomainsCount), + DomainsBlocked: responseJson.Response.Stats.BlockedZones + responseJson.Response.Stats.BlockListZones, } if stats.TotalQueries <= 0 { From facbf6f5294d7764b2c6c0c7ff1bf30fc691f1f9 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:08:36 +0000 Subject: [PATCH 004/142] Remove mention of env variable syntax --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a1f3523..1bd4a26 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1778,16 +1778,16 @@ Either `adguard`, `pihole`, or `technitium`. Whether to allow invalid/self-signed certificates when making the request to the service. ##### `url` -The base URL of the service. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +The base URL of the service. ##### `username` -Only required when using AdGuard Home. The username used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using AdGuard Home. The username used to log into the admin dashboard. ##### `password` -Only required when using AdGuard Home. The password used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using AdGuard Home. The password used to log into the admin dashboard. ##### `token` -Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`. ##### `hide-graph` Whether to hide the graph showing the number of queries over time. From f9209406fbfa55cec8209caff14299f5d4dc1a7d Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:18:27 +0000 Subject: [PATCH 005/142] Reduce duplication of constants --- internal/glance/widget-dns-stats.go | 68 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index a137228..34f1dfe 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -14,6 +14,12 @@ import ( var dnsStatsWidgetTemplate = mustParseTemplate("dns-stats.html", "widget-base.html") +const ( + dnsStatsBars = 8 + dnsStatsHoursSpan = 24 + dnsStatsHoursPerBar int = dnsStatsHoursSpan / dnsStatsBars +) + type dnsStatsWidget struct { widgetBase `yaml:",inline"` @@ -186,31 +192,27 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor queriesSeries := responseJson.QueriesSeries blockedSeries := responseJson.BlockedSeries - const bars = 8 - const hoursSpan = 24 - const hoursPerBar int = hoursSpan / bars - - if len(queriesSeries) > hoursSpan { - queriesSeries = queriesSeries[len(queriesSeries)-hoursSpan:] - } else if len(queriesSeries) < hoursSpan { - queriesSeries = append(make([]int, hoursSpan-len(queriesSeries)), queriesSeries...) + if len(queriesSeries) > dnsStatsHoursSpan { + queriesSeries = queriesSeries[len(queriesSeries)-dnsStatsHoursSpan:] + } else if len(queriesSeries) < dnsStatsHoursSpan { + queriesSeries = append(make([]int, dnsStatsHoursSpan-len(queriesSeries)), queriesSeries...) } - if len(blockedSeries) > hoursSpan { - blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:] - } else if len(blockedSeries) < hoursSpan { - blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...) + if len(blockedSeries) > dnsStatsHoursSpan { + blockedSeries = blockedSeries[len(blockedSeries)-dnsStatsHoursSpan:] + } else if len(blockedSeries) < dnsStatsHoursSpan { + blockedSeries = append(make([]int, dnsStatsHoursSpan-len(blockedSeries)), blockedSeries...) } maxQueriesInSeries := 0 - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { queries := 0 blocked := 0 - for j := 0; j < hoursPerBar; j++ { - queries += queriesSeries[i*hoursPerBar+j] - blocked += blockedSeries[i*hoursPerBar+j] + for j := 0; j < dnsStatsHoursPerBar; j++ { + queries += queriesSeries[i*dnsStatsHoursPerBar+j] + blocked += blockedSeries[i*dnsStatsHoursPerBar+j] } stats.Series[i] = dnsStatsSeries{ @@ -227,7 +229,7 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor } } - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) } @@ -409,7 +411,6 @@ type technitiumStatsResponse struct { } func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) { - if token == "" { return nil, errors.New("missing API token") } @@ -480,31 +481,27 @@ func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, } } - const bars = 8 - const hoursSpan = 24 - const hoursPerBar int = hoursSpan / bars - - if len(queriesSeries) > hoursSpan { - queriesSeries = queriesSeries[len(queriesSeries)-hoursSpan:] - } else if len(queriesSeries) < hoursSpan { - queriesSeries = append(make([]int, hoursSpan-len(queriesSeries)), queriesSeries...) + if len(queriesSeries) > dnsStatsHoursSpan { + queriesSeries = queriesSeries[len(queriesSeries)-dnsStatsHoursSpan:] + } else if len(queriesSeries) < dnsStatsHoursSpan { + queriesSeries = append(make([]int, dnsStatsHoursSpan-len(queriesSeries)), queriesSeries...) } - if len(blockedSeries) > hoursSpan { - blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:] - } else if len(blockedSeries) < hoursSpan { - blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...) + if len(blockedSeries) > dnsStatsHoursSpan { + blockedSeries = blockedSeries[len(blockedSeries)-dnsStatsHoursSpan:] + } else if len(blockedSeries) < dnsStatsHoursSpan { + blockedSeries = append(make([]int, dnsStatsHoursSpan-len(blockedSeries)), blockedSeries...) } maxQueriesInSeries := 0 - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { queries := 0 blocked := 0 - for j := 0; j < hoursPerBar; j++ { - queries += queriesSeries[i*hoursPerBar+j] - blocked += blockedSeries[i*hoursPerBar+j] + for j := 0; j < dnsStatsHoursPerBar; j++ { + queries += queriesSeries[i*dnsStatsHoursPerBar+j] + blocked += blockedSeries[i*dnsStatsHoursPerBar+j] } stats.Series[i] = dnsStatsSeries{ @@ -521,10 +518,9 @@ func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, } } - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) } return stats, nil - } From fcccb7eb387b9fb5480fe80992a4a3be407133ff Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:20:38 +0000 Subject: [PATCH 006/142] Update error message --- internal/glance/widget-dns-stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index 34f1dfe..68b6ac2 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -59,7 +59,7 @@ func (widget *dnsStatsWidget) initialize() error { case "pihole": case "technitium": default: - return errors.New("service must be either 'adguard' or 'pihole'") + return errors.New("service must be either 'adguard', 'pihole', or 'technitium'") } return nil From c00d937f4c3e69245c988ccc5cbe4293c441ad32 Mon Sep 17 00:00:00 2001 From: ejsadiarin Date: Wed, 19 Feb 2025 17:28:13 +0800 Subject: [PATCH 007/142] feat(monitor): add basic-auth feature for protected sites this closes [issue #316](https://github.com/glanceapp/glance/issues/316) Furthermore, this could be expanded to also pass the configured basic auth credentials to the request when the user clicks on the specific monitor widget --- internal/glance/widget-monitor.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/internal/glance/widget-monitor.go b/internal/glance/widget-monitor.go index 76f0d45..ee4cb85 100644 --- a/internal/glance/widget-monitor.go +++ b/internal/glance/widget-monitor.go @@ -28,6 +28,10 @@ type monitorWidget struct { StatusText string `yaml:"-"` StatusStyle string `yaml:"-"` AltStatusCodes []int `yaml:"alt-status-codes"` + BasicAuth struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + } `yaml:"basic-auth"` } `yaml:"sites"` Style string `yaml:"style"` ShowFailingOnly bool `yaml:"show-failing-only"` @@ -45,6 +49,10 @@ func (widget *monitorWidget) update(ctx context.Context) { for i := range widget.Sites { requests[i] = widget.Sites[i].SiteStatusRequest + if widget.Sites[i].BasicAuth.Username != "" || widget.Sites[i].BasicAuth.Password != "" { + requests[i].Username = widget.Sites[i].BasicAuth.Username + requests[i].Password = widget.Sites[i].BasicAuth.Password + } } statuses, err := fetchStatusForSites(requests) @@ -118,6 +126,8 @@ type SiteStatusRequest struct { DefaultURL string `yaml:"url"` CheckURL string `yaml:"check-url"` AllowInsecure bool `yaml:"allow-insecure"` + Username string `yaml:"-"` + Password string `yaml:"-"` } type siteStatus struct { @@ -141,6 +151,10 @@ func fetchSiteStatusTask(statusRequest *SiteStatusRequest) (siteStatus, error) { }, nil } + if statusRequest.Username != "" || statusRequest.Password != "" { + request.SetBasicAuth(statusRequest.Username, statusRequest.Password) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) defer cancel() request = request.WithContext(ctx) From 5b45751c67497ae00bd96cff1cd2538165764e77 Mon Sep 17 00:00:00 2001 From: ejsadiarin Date: Wed, 19 Feb 2025 17:40:56 +0800 Subject: [PATCH 008/142] docs(monitor): add documentation for basic-auth feature --- docs/configuration.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 844d34d..b14c568 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1476,6 +1476,7 @@ Properties for each site: | allow-insecure | boolean | no | false | | same-tab | boolean | no | false | | alt-status-codes | array | no | | +| basic-auth | object | no | | `title` @@ -1524,6 +1525,16 @@ alt-status-codes: - 403 ``` +`basic-auth` + +HTTP Basic Authentication credentials for protected sites. + +```yaml +basic-auth: + usename: your-username + password: your-password +``` + ### Releases Display a list of latest releases for specific repositories on Github, GitLab, Codeberg or Docker Hub. From dac0d15e785fea5843fc5a0aee8e2e6d880bf13c Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 22 Feb 2025 13:29:00 +0000 Subject: [PATCH 009/142] Update implementation --- internal/glance/widget-monitor.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/internal/glance/widget-monitor.go b/internal/glance/widget-monitor.go index ee4cb85..e42a710 100644 --- a/internal/glance/widget-monitor.go +++ b/internal/glance/widget-monitor.go @@ -28,10 +28,6 @@ type monitorWidget struct { StatusText string `yaml:"-"` StatusStyle string `yaml:"-"` AltStatusCodes []int `yaml:"alt-status-codes"` - BasicAuth struct { - Username string `yaml:"username"` - Password string `yaml:"password"` - } `yaml:"basic-auth"` } `yaml:"sites"` Style string `yaml:"style"` ShowFailingOnly bool `yaml:"show-failing-only"` @@ -49,10 +45,6 @@ func (widget *monitorWidget) update(ctx context.Context) { for i := range widget.Sites { requests[i] = widget.Sites[i].SiteStatusRequest - if widget.Sites[i].BasicAuth.Username != "" || widget.Sites[i].BasicAuth.Password != "" { - requests[i].Username = widget.Sites[i].BasicAuth.Username - requests[i].Password = widget.Sites[i].BasicAuth.Password - } } statuses, err := fetchStatusForSites(requests) @@ -126,8 +118,10 @@ type SiteStatusRequest struct { DefaultURL string `yaml:"url"` CheckURL string `yaml:"check-url"` AllowInsecure bool `yaml:"allow-insecure"` - Username string `yaml:"-"` - Password string `yaml:"-"` + BasicAuth struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + } `yaml:"basic-auth"` } type siteStatus struct { @@ -151,8 +145,8 @@ func fetchSiteStatusTask(statusRequest *SiteStatusRequest) (siteStatus, error) { }, nil } - if statusRequest.Username != "" || statusRequest.Password != "" { - request.SetBasicAuth(statusRequest.Username, statusRequest.Password) + if statusRequest.BasicAuth.Username != "" || statusRequest.BasicAuth.Password != "" { + request.SetBasicAuth(statusRequest.BasicAuth.Username, statusRequest.BasicAuth.Password) } ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) From 9df9673e8473067c8048cd0ac5c3a954d74bd807 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:25:01 +0000 Subject: [PATCH 010/142] Add alternative include syntax Also make it the new recommended way for doing includes --- docs/configuration.md | 16 ++++++++-------- internal/glance/config.go | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8289822..8c97123 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -93,13 +93,13 @@ something: \${NOT_AN_ENV_VAR} ``` ### Including other config files -Including config files from within your main config file is supported. This is done via the `!include` directive along with a relative or absolute path to the file you want to include. If the path is relative, it will be relative to the main config file. Additionally, environment variables can be used within included files, and changes to the included files will trigger an automatic reload. Example: +Including config files from within your main config file is supported. This is done via the `$include` directive along with a relative or absolute path to the file you want to include. If the path is relative, it will be relative to the main config file. Additionally, environment variables can be used within included files, and changes to the included files will trigger an automatic reload. Example: ```yaml pages: - !include: home.yml - !include: videos.yml - !include: homelab.yml + - $include: home.yml + - $include: videos.yml + - $include: homelab.yml ``` The file you are including should not have any additional indentation, its values should be at the top level and the appropriate amount of indentation will be added automatically depending on where the file is included. Example: @@ -112,14 +112,14 @@ pages: columns: - size: full widgets: - !include: rss.yml + $include: rss.yml - name: News columns: - size: full widgets: - type: group widgets: - !include: rss.yml + $include: rss.yml - type: reddit subreddit: news ``` @@ -133,9 +133,9 @@ pages: - url: ${RSS_URL} ``` -The `!include` directive can be used anywhere in the config file, not just in the `pages` property, however it must be on its own line and have the appropriate indentation. +The `$include` directive can be used anywhere in the config file, not just in the `pages` property, however it must be on its own line and have the appropriate indentation. -If you encounter YAML parsing errors when using the `!include` directive, the reported line numbers will likely be incorrect. This is because the inclusion of files is done before the YAML is parsed, as YAML itself does not support file inclusion. To help with debugging in cases like this, you can use the `config:print` command and pipe it into `less -N` to see the full config file with includes resolved and line numbers added: +If you encounter YAML parsing errors when using the `$include` directive, the reported line numbers will likely be incorrect. This is because the inclusion of files is done before the YAML is parsed, as YAML itself does not support file inclusion. To help with debugging in cases like this, you can use the `config:print` command and pipe it into `less -N` to see the full config file with includes resolved and line numbers added: ```sh glance --config /path/to/glance.yml config:print | less -N diff --git a/internal/glance/config.go b/internal/glance/config.go index 0d424a2..e7d922f 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -144,7 +144,7 @@ func formatWidgetInitError(err error, w widget) error { return fmt.Errorf("%s widget: %v", w.GetType(), err) } -var includePattern = regexp.MustCompile(`(?m)^(\s*)!include:\s*(.+)$`) +var includePattern = regexp.MustCompile(`(?m)^([ \t]*)(?:-[ \t]*)?(?:!|\$)include:[ \t]*(.+)$`) func parseYAMLIncludes(mainFilePath string) ([]byte, map[string]struct{}, error) { mainFileContents, err := os.ReadFile(mainFilePath) From 19a89645a11e2fdc1c2b91d8a8e6e0d7460ba18e Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:11:03 +0000 Subject: [PATCH 011/142] Add support for nested includes --- internal/glance/config.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/glance/config.go b/internal/glance/config.go index e7d922f..e77dba8 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -147,18 +147,24 @@ func formatWidgetInitError(err error, w widget) error { var includePattern = regexp.MustCompile(`(?m)^([ \t]*)(?:-[ \t]*)?(?:!|\$)include:[ \t]*(.+)$`) func parseYAMLIncludes(mainFilePath string) ([]byte, map[string]struct{}, error) { + return recursiveParseYAMLIncludes(mainFilePath, nil) +} + +func recursiveParseYAMLIncludes(mainFilePath string, includes map[string]struct{}) ([]byte, map[string]struct{}, error) { mainFileContents, err := os.ReadFile(mainFilePath) if err != nil { - return nil, nil, fmt.Errorf("reading main YAML file: %w", err) + return nil, nil, fmt.Errorf("reading %s: %w", mainFilePath, err) } mainFileAbsPath, err := filepath.Abs(mainFilePath) if err != nil { - return nil, nil, fmt.Errorf("getting absolute path of main YAML file: %w", err) + return nil, nil, fmt.Errorf("getting absolute path of %s: %w", mainFilePath, err) } mainFileDir := filepath.Dir(mainFileAbsPath) - includes := make(map[string]struct{}) + if includes == nil { + includes = make(map[string]struct{}) + } var includesLastErr error mainFileContents = includePattern.ReplaceAllFunc(mainFileContents, func(match []byte) []byte { @@ -181,13 +187,14 @@ func parseYAMLIncludes(mainFilePath string) ([]byte, map[string]struct{}, error) var fileContents []byte var err error - fileContents, err = os.ReadFile(includeFilePath) + includes[includeFilePath] = struct{}{} + + fileContents, includes, err = recursiveParseYAMLIncludes(includeFilePath, includes) if err != nil { - includesLastErr = fmt.Errorf("reading included file %s: %w", includeFilePath, err) + includesLastErr = fmt.Errorf("recursively parsing included file %s: %w", includeFilePath, err) return nil } - includes[includeFilePath] = struct{}{} return []byte(prefixStringLines(indent, string(fileContents))) }) From 5d12d934b83b7d56f5732821b4add7453712a8f5 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:11:44 +0000 Subject: [PATCH 012/142] Use new range syntax --- internal/glance/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/glance/config.go b/internal/glance/config.go index e77dba8..2d3cdba 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -315,7 +315,7 @@ func configFilesWatcher( // wait for file to maybe get created again // see https://github.com/glanceapp/glance/pull/358 - for i := 0; i < 10; i++ { + for range 10 { if _, err := os.Stat(event.Name); err == nil { break } From 2738613344ac5fc5463ebc306f986a74d342537b Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:12:07 +0000 Subject: [PATCH 013/142] Improve error message when widget type not specified --- internal/glance/widget.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/glance/widget.go b/internal/glance/widget.go index c15368a..6f0da6b 100644 --- a/internal/glance/widget.go +++ b/internal/glance/widget.go @@ -76,7 +76,10 @@ func newWidget(widgetType string) (widget, error) { case "server-stats": w = &serverStatsWidget{} default: - return nil, fmt.Errorf("unknown widget type: %s", widgetType) + return nil, fmt.Errorf( + "unknown widget type: %s", + ternary(widgetType == "", "'type' property is empty or not specified", widgetType), + ) } w.setID(widgetIDCounter.Add(1)) From ce293ed8915a0f0d2a1214f1a45d8b68430c8176 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 27 Feb 2025 07:22:18 +0000 Subject: [PATCH 014/142] Prevent infinite config include recursion --- internal/glance/config.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/glance/config.go b/internal/glance/config.go index 2d3cdba..5a1af22 100644 --- a/internal/glance/config.go +++ b/internal/glance/config.go @@ -17,6 +17,8 @@ import ( "gopkg.in/yaml.v3" ) +const CONFIG_INCLUDE_RECURSION_DEPTH_LIMIT = 20 + type config struct { Server struct { Host string `yaml:"host"` @@ -147,10 +149,14 @@ func formatWidgetInitError(err error, w widget) error { var includePattern = regexp.MustCompile(`(?m)^([ \t]*)(?:-[ \t]*)?(?:!|\$)include:[ \t]*(.+)$`) func parseYAMLIncludes(mainFilePath string) ([]byte, map[string]struct{}, error) { - return recursiveParseYAMLIncludes(mainFilePath, nil) + return recursiveParseYAMLIncludes(mainFilePath, nil, 0) } -func recursiveParseYAMLIncludes(mainFilePath string, includes map[string]struct{}) ([]byte, map[string]struct{}, error) { +func recursiveParseYAMLIncludes(mainFilePath string, includes map[string]struct{}, depth int) ([]byte, map[string]struct{}, error) { + if depth > CONFIG_INCLUDE_RECURSION_DEPTH_LIMIT { + return nil, nil, fmt.Errorf("recursion depth limit of %d reached", CONFIG_INCLUDE_RECURSION_DEPTH_LIMIT) + } + mainFileContents, err := os.ReadFile(mainFilePath) if err != nil { return nil, nil, fmt.Errorf("reading %s: %w", mainFilePath, err) @@ -189,9 +195,9 @@ func recursiveParseYAMLIncludes(mainFilePath string, includes map[string]struct{ includes[includeFilePath] = struct{}{} - fileContents, includes, err = recursiveParseYAMLIncludes(includeFilePath, includes) + fileContents, includes, err = recursiveParseYAMLIncludes(includeFilePath, includes, depth+1) if err != nil { - includesLastErr = fmt.Errorf("recursively parsing included file %s: %w", includeFilePath, err) + includesLastErr = err return nil } From 948289a03895d46bc4e32c0cb259bbfb7df645e1 Mon Sep 17 00:00:00 2001 From: Ralph Ocdol Date: Fri, 28 Feb 2025 08:48:07 +0800 Subject: [PATCH 015/142] feat: add parameters and array parameters support --- docs/configuration.md | 12 ++++++++ internal/glance/widget-custom-api.go | 42 +++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8c97123..c4ed1b8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1294,6 +1294,7 @@ Examples: | headers | key (string) & value (string) | no | | | frameless | boolean | no | false | | template | string | yes | | +| parameters | key & value | no | | ##### `url` The URL to fetch the data from. It must be accessible from the server that Glance is running on. @@ -1313,6 +1314,17 @@ When set to `true`, removes the border and padding around the widget. ##### `template` The template that will be used to display the data. It relies on Go's `html/template` package so it's recommended to go through [its documentation](https://pkg.go.dev/text/template) to understand how to do basic things such as conditionals, loops, etc. In addition, it also uses [tidwall's gjson](https://github.com/tidwall/gjson) package to parse the JSON data so it's worth going through its documentation if you want to use more advanced JSON selectors. You can view additional examples with explanations and function definitions [here](custom-api.md). +##### `parameters` +A list of keys and values that will be sent to the custom-api as query paramters. + +```yaml +parameters: + param1: value1 + param2: + - item1 + - item2 +``` + ### Extension Display a widget provided by an external source (3rd party). If you want to learn more about developing extensions, checkout the [extensions documentation](extensions.md) (WIP). diff --git a/internal/glance/widget-custom-api.go b/internal/glance/widget-custom-api.go index c8e3773..9e58f09 100644 --- a/internal/glance/widget-custom-api.go +++ b/internal/glance/widget-custom-api.go @@ -10,6 +10,7 @@ import ( "log/slog" "math" "net/http" + "net/url" "time" "github.com/tidwall/gjson" @@ -19,13 +20,14 @@ var customAPIWidgetTemplate = mustParseTemplate("custom-api.html", "widget-base. type customAPIWidget struct { widgetBase `yaml:",inline"` - URL string `yaml:"url"` - Template string `yaml:"template"` - Frameless bool `yaml:"frameless"` - Headers map[string]string `yaml:"headers"` - APIRequest *http.Request `yaml:"-"` - compiledTemplate *template.Template `yaml:"-"` - CompiledHTML template.HTML `yaml:"-"` + URL string `yaml:"url"` + Template string `yaml:"template"` + Frameless bool `yaml:"frameless"` + Headers map[string]string `yaml:"headers"` + Parameters map[string]interface{} `yaml:"parameters"` + APIRequest *http.Request `yaml:"-"` + compiledTemplate *template.Template `yaml:"-"` + CompiledHTML template.HTML `yaml:"-"` } func (widget *customAPIWidget) initialize() error { @@ -51,6 +53,32 @@ func (widget *customAPIWidget) initialize() error { return err } + query := url.Values{} + + for key, value := range widget.Parameters { + switch v := value.(type) { + case string: + query.Add(key, v) + case int, int8, int16, int32, int64, float32, float64: + query.Add(key, fmt.Sprintf("%v", v)) + case []string: + for _, item := range v { + query.Add(key, item) + } + case []interface{}: + for _, item := range v { + switch item := item.(type) { + case string: + query.Add(key, item) + case int, int8, int16, int32, int64, float32, float64: + query.Add(key, fmt.Sprintf("%v", item)) + } + } + } + } + + req.URL.RawQuery = query.Encode() + for key, value := range widget.Headers { req.Header.Add(key, value) } From 8da26ab4095a40a6866de551c52bb22e23b85563 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:29:28 +0000 Subject: [PATCH 016/142] Make query parameters field reusable --- internal/glance/config-fields.go | 52 ++++++++++++++++++++++++++++ internal/glance/widget-custom-api.go | 43 +++++------------------ 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/internal/glance/config-fields.go b/internal/glance/config-fields.go index f3c836e..966b366 100644 --- a/internal/glance/config-fields.go +++ b/internal/glance/config-fields.go @@ -219,3 +219,55 @@ func (p *proxyOptionsField) UnmarshalYAML(node *yaml.Node) error { return nil } + +type queryParametersField map[string][]string + +func (q *queryParametersField) UnmarshalYAML(node *yaml.Node) error { + var decoded map[string]any + + if err := node.Decode(&decoded); err != nil { + return err + } + + *q = make(queryParametersField) + + for key, value := range decoded { + switch v := value.(type) { + case string: + (*q)[key] = []string{v} + case int, int8, int16, int32, int64, float32, float64: + (*q)[key] = []string{fmt.Sprintf("%v", v)} + case []string: + (*q)[key] = append((*q)[key], v...) + case []any: + for _, item := range v { + switch item := item.(type) { + case string: + (*q)[key] = append((*q)[key], item) + case int, int8, int16, int32, int64, float32, float64: + (*q)[key] = append((*q)[key], fmt.Sprintf("%v", item)) + case bool: + (*q)[key] = append((*q)[key], fmt.Sprintf("%t", item)) + default: + return fmt.Errorf("invalid query parameter value type: %T", item) + } + } + default: + return fmt.Errorf("invalid query parameter value type: %T", value) + } + } + + return nil +} + +func (q *queryParametersField) toQueryString() string { + query := url.Values{} + + for key, values := range *q { + for _, value := range values { + query.Add(key, value) + } + } + + return query.Encode() +} diff --git a/internal/glance/widget-custom-api.go b/internal/glance/widget-custom-api.go index 9e58f09..1c50ad6 100644 --- a/internal/glance/widget-custom-api.go +++ b/internal/glance/widget-custom-api.go @@ -10,7 +10,6 @@ import ( "log/slog" "math" "net/http" - "net/url" "time" "github.com/tidwall/gjson" @@ -20,14 +19,14 @@ var customAPIWidgetTemplate = mustParseTemplate("custom-api.html", "widget-base. type customAPIWidget struct { widgetBase `yaml:",inline"` - URL string `yaml:"url"` - Template string `yaml:"template"` - Frameless bool `yaml:"frameless"` - Headers map[string]string `yaml:"headers"` - Parameters map[string]interface{} `yaml:"parameters"` - APIRequest *http.Request `yaml:"-"` - compiledTemplate *template.Template `yaml:"-"` - CompiledHTML template.HTML `yaml:"-"` + URL string `yaml:"url"` + Template string `yaml:"template"` + Frameless bool `yaml:"frameless"` + Headers map[string]string `yaml:"headers"` + Parameters queryParametersField `yaml:"parameters"` + APIRequest *http.Request `yaml:"-"` + compiledTemplate *template.Template `yaml:"-"` + CompiledHTML template.HTML `yaml:"-"` } func (widget *customAPIWidget) initialize() error { @@ -53,31 +52,7 @@ func (widget *customAPIWidget) initialize() error { return err } - query := url.Values{} - - for key, value := range widget.Parameters { - switch v := value.(type) { - case string: - query.Add(key, v) - case int, int8, int16, int32, int64, float32, float64: - query.Add(key, fmt.Sprintf("%v", v)) - case []string: - for _, item := range v { - query.Add(key, item) - } - case []interface{}: - for _, item := range v { - switch item := item.(type) { - case string: - query.Add(key, item) - case int, int8, int16, int32, int64, float32, float64: - query.Add(key, fmt.Sprintf("%v", item)) - } - } - } - } - - req.URL.RawQuery = query.Encode() + req.URL.RawQuery = widget.Parameters.toQueryString() for key, value := range widget.Headers { req.Header.Add(key, value) From acddaf07db7466c7f1ab73686c81a9380f9ec349 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:29:56 +0000 Subject: [PATCH 017/142] Add note to docs --- docs/configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index c4ed1b8..a0ff491 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1317,6 +1317,10 @@ The template that will be used to display the data. It relies on Go's `html/temp ##### `parameters` A list of keys and values that will be sent to the custom-api as query paramters. +> [!NOTE] +> +> Setting this property will override any query parameters that are already in the URL. + ```yaml parameters: param1: value1 From 49668d4ba9153bd05ec169ca6d3876396bda79ad Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:30:20 +0000 Subject: [PATCH 018/142] Also apply to extension widget --- internal/glance/widget-extension.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/internal/glance/widget-extension.go b/internal/glance/widget-extension.go index 72a4a09..47034f3 100644 --- a/internal/glance/widget-extension.go +++ b/internal/glance/widget-extension.go @@ -19,12 +19,12 @@ const extensionWidgetDefaultTitle = "Extension" type extensionWidget struct { widgetBase `yaml:",inline"` - URL string `yaml:"url"` - FallbackContentType string `yaml:"fallback-content-type"` - Parameters map[string]string `yaml:"parameters"` - AllowHtml bool `yaml:"allow-potentially-dangerous-html"` - Extension extension `yaml:"-"` - cachedHTML template.HTML `yaml:"-"` + URL string `yaml:"url"` + FallbackContentType string `yaml:"fallback-content-type"` + Parameters queryParametersField `yaml:"parameters"` + AllowHtml bool `yaml:"allow-potentially-dangerous-html"` + Extension extension `yaml:"-"` + cachedHTML template.HTML `yaml:"-"` } func (widget *extensionWidget) initialize() error { @@ -82,10 +82,10 @@ const ( ) type extensionRequestOptions struct { - URL string `yaml:"url"` - FallbackContentType string `yaml:"fallback-content-type"` - Parameters map[string]string `yaml:"parameters"` - AllowHtml bool `yaml:"allow-potentially-dangerous-html"` + URL string `yaml:"url"` + FallbackContentType string `yaml:"fallback-content-type"` + Parameters queryParametersField `yaml:"parameters"` + AllowHtml bool `yaml:"allow-potentially-dangerous-html"` } type extension struct { @@ -109,14 +109,7 @@ func convertExtensionContent(options extensionRequestOptions, content []byte, co func fetchExtension(options extensionRequestOptions) (extension, error) { request, _ := http.NewRequest("GET", options.URL, nil) - - query := url.Values{} - - for key, value := range options.Parameters { - query.Set(key, value) - } - - request.URL.RawQuery = query.Encode() + request.URL.RawQuery = options.Parameters.toQueryString() response, err := http.DefaultClient.Do(request) if err != nil { From 474255c9859829344296dbdd6f22321cef558611 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 27 Feb 2025 15:18:24 +0000 Subject: [PATCH 019/142] Tweak error message --- internal/glance/widget.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/glance/widget.go b/internal/glance/widget.go index 6f0da6b..77d7781 100644 --- a/internal/glance/widget.go +++ b/internal/glance/widget.go @@ -18,6 +18,10 @@ import ( var widgetIDCounter atomic.Uint64 func newWidget(widgetType string) (widget, error) { + if widgetType == "" { + return nil, errors.New("widget 'type' property is empty or not specified") + } + var w widget switch widgetType { @@ -76,10 +80,7 @@ func newWidget(widgetType string) (widget, error) { case "server-stats": w = &serverStatsWidget{} default: - return nil, fmt.Errorf( - "unknown widget type: %s", - ternary(widgetType == "", "'type' property is empty or not specified", widgetType), - ) + return nil, fmt.Errorf("unknown widget type: %s", widgetType) } w.setID(widgetIDCounter.Add(1)) @@ -107,7 +108,7 @@ func (w *widgets) UnmarshalYAML(node *yaml.Node) error { widget, err := newWidget(meta.Type) if err != nil { - return err + return fmt.Errorf("line %d: %w", node.Line, err) } if err = node.Decode(widget); err != nil { From 31ecd91f7c7aa0b325f8a95bcc934d89ffe08b13 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:43:33 +0000 Subject: [PATCH 020/142] Fix failing to parse empty response body in custom api widget --- internal/glance/widget-custom-api.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/glance/widget-custom-api.go b/internal/glance/widget-custom-api.go index 1c50ad6..5ea7bf7 100644 --- a/internal/glance/widget-custom-api.go +++ b/internal/glance/widget-custom-api.go @@ -10,6 +10,7 @@ import ( "log/slog" "math" "net/http" + "strings" "time" "github.com/tidwall/gjson" @@ -90,9 +91,9 @@ func fetchAndParseCustomAPI(req *http.Request, tmpl *template.Template) (templat return emptyBody, err } - body := string(bodyBytes) + body := strings.TrimSpace(string(bodyBytes)) - if !gjson.Valid(body) { + if body != "" && !gjson.Valid(body) { truncatedBody, isTruncated := limitStringLength(body, 100) if isTruncated { truncatedBody += "... " From 6c8859863a2a5b92a21db84f9c5636a90d2e999f Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 2 Mar 2025 00:09:28 +0000 Subject: [PATCH 021/142] Add description property to bookmarks widget links --- docs/configuration.md | 1 + internal/glance/templates/bookmarks.html | 29 +++++++++++++++--------- internal/glance/widget-bookmarks.go | 7 +++--- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a0ff491..0d90c9d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2057,6 +2057,7 @@ An array of groups which can optionally have a title and a custom color. | ---- | ---- | -------- | ------- | | title | string | yes | | | url | string | yes | | +| description | string | no | | | icon | string | no | | | same-tab | boolean | no | false | | hide-arrow | boolean | no | false | diff --git a/internal/glance/templates/bookmarks.html b/internal/glance/templates/bookmarks.html index f247bdb..1952cdb 100644 --- a/internal/glance/templates/bookmarks.html +++ b/internal/glance/templates/bookmarks.html @@ -2,22 +2,29 @@ {{ define "widget-content" }}
- {{ range .Groups }} + {{- range .Groups }}
- {{ if ne .Title "" }}
{{ .Title }}
{{ end }} + {{- if ne .Title "" }} +
{{ .Title }}
+ {{- end }}
    - {{ range .Links }} -
  • - {{ if ne "" .Icon.URL }} -
    - + {{- range .Links }} +
  • +
    + {{- if ne "" .Icon.URL }} +
    + +
    + {{- end }} + {{ .Title }}
    - {{ end }} - {{ .Title }} + {{- if .Description }} +
    {{ .Description }}
    + {{- end }}
  • - {{ end }} + {{- end }}
- {{ end }} + {{- end }}
{{ end }} diff --git a/internal/glance/widget-bookmarks.go b/internal/glance/widget-bookmarks.go index 4f3d26e..2245e2e 100644 --- a/internal/glance/widget-bookmarks.go +++ b/internal/glance/widget-bookmarks.go @@ -16,9 +16,10 @@ type bookmarksWidget struct { HideArrow bool `yaml:"hide-arrow"` Target string `yaml:"target"` Links []struct { - Title string `yaml:"title"` - URL string `yaml:"url"` - Icon customIconField `yaml:"icon"` + Title string `yaml:"title"` + URL string `yaml:"url"` + Description string `yaml:"description"` + Icon customIconField `yaml:"icon"` // we need a pointer to bool to know whether a value was provided, // however there's no way to dereference a pointer in a template so // {{ if not .SameTab }} would return true for any non-nil pointer From e373eeeed357d0c879a9648e26e2b8b0c974fc03 Mon Sep 17 00:00:00 2001 From: Ralph Ocdol Date: Mon, 10 Mar 2025 17:49:38 +0800 Subject: [PATCH 022/142] fix: full width clickable link for monitor-site (#405) * feat: full width clickable link for monitor-site * refactor * Use grow instead of width-100 --------- Co-authored-by: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> --- internal/glance/templates/monitor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/glance/templates/monitor.html b/internal/glance/templates/monitor.html index 7e95b99..6398921 100644 --- a/internal/glance/templates/monitor.html +++ b/internal/glance/templates/monitor.html @@ -24,7 +24,7 @@ {{ if .Icon.URL }} {{ end }} -
+
{{ .Title }}