From bae95a5e07119fb1e24907a4ed98449edafb7daa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Veronika=20Bu=C5=A1ov=C3=A1?= Date: Sun, 15 Sep 2024 00:29:46 +0200 Subject: [PATCH 001/272] Add important details about the server.base-url property --- docs/configuration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index fb119a5..116f8da 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -138,6 +138,10 @@ A number between 1 and 65,535, so long as that port isn't already used by anythi #### `base-url` The base URL that Glance is hosted under. No need to specify this unless you're using a reverse proxy and are hosting Glance under a directory. If that's the case then you can set this value to `/glance` or whatever the directory is called. Note that the forward slash (`/`) in the beginning is required unless you specify the full domain and path. +> [!IMPORTANT] +> You need to strip the `base-url` prefix before forwarding the request to the Glance server. +> In Caddy you can do this using [`handle_path`](https://caddyserver.com/docs/caddyfile/directives/handle_path) or [`uri strip_prefix`](https://caddyserver.com/docs/caddyfile/directives/uri). + #### `assets-path` The path to a directory that will be served by the server under the `/assets/` path. This is handy for widgets like the Monitor where you have to specify an icon URL and you want to self host all the icons rather than pointing to an external source. From 28167403a46b71de8a3eb94920ca0d5edcce8cf1 Mon Sep 17 00:00:00 2001 From: SimJunYou Date: Thu, 19 Sep 2024 17:43:43 +0800 Subject: [PATCH 002/272] Cover edge case when Pihole's privacy settings are enabled --- internal/feed/pihole.go | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/internal/feed/pihole.go b/internal/feed/pihole.go index 3849267..5a4b7f8 100644 --- a/internal/feed/pihole.go +++ b/internal/feed/pihole.go @@ -18,6 +18,17 @@ type piholeStatsResponse struct { DomainsBlocked int `json:"domains_being_blocked"` } +// If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array +// Use alternate struct without that field to avoid error when unmarshalling +type piholeStatsResponsePrivate struct { + TotalQueries int `json:"dns_queries_today"` + QueriesSeries map[int64]int `json:"domains_over_time"` + BlockedQueries int `json:"ads_blocked_today"` + BlockedSeries map[int64]int `json:"ads_over_time"` + BlockedPercentage float64 `json:"ads_percentage_today"` + DomainsBlocked int `json:"domains_being_blocked"` +} + func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { if token == "" { return nil, errors.New("missing API token") @@ -31,13 +42,27 @@ func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { if err != nil { return nil, err } - + responseJson, err := decodeJsonFromRequest[piholeStatsResponse](defaultClient, request) - + if err != nil { - return nil, err + // Refer to piholeStatsResponsePrivate above + responseJsonPriv, err := + decodeJsonFromRequest[piholeStatsResponsePrivate](defaultClient, request) + if err != nil { + return nil, err + } + + // Copy the results back to responseJson, leaving the TopBlockedDomains field empty + responseJson.TotalQueries = responseJsonPriv.TotalQueries + responseJson.QueriesSeries = responseJsonPriv.QueriesSeries + responseJson.BlockedQueries = responseJsonPriv.BlockedQueries + responseJson.BlockedSeries = responseJsonPriv.BlockedSeries + responseJson.BlockedPercentage = responseJsonPriv.BlockedPercentage + responseJson.TopBlockedDomains = make(map[string]int) + responseJson.DomainsBlocked = responseJsonPriv.DomainsBlocked } - + stats := &DNSStats{ TotalQueries: responseJson.TotalQueries, BlockedQueries: responseJson.BlockedQueries, From 672547cd07d0620ae988ec2ff96ac298bcb07778 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:24:44 +0100 Subject: [PATCH 003/272] Switch to using custom type for pihole's top blocked domains --- internal/feed/pihole.go | 62 +++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/internal/feed/pihole.go b/internal/feed/pihole.go index 5a4b7f8..3c7f1b5 100644 --- a/internal/feed/pihole.go +++ b/internal/feed/pihole.go @@ -1,6 +1,7 @@ package feed import ( + "encoding/json" "errors" "log/slog" "net/http" @@ -9,24 +10,33 @@ import ( ) type piholeStatsResponse struct { - TotalQueries int `json:"dns_queries_today"` - QueriesSeries map[int64]int `json:"domains_over_time"` - BlockedQueries int `json:"ads_blocked_today"` - BlockedSeries map[int64]int `json:"ads_over_time"` - BlockedPercentage float64 `json:"ads_percentage_today"` - TopBlockedDomains map[string]int `json:"top_ads"` - DomainsBlocked int `json:"domains_being_blocked"` + TotalQueries int `json:"dns_queries_today"` + QueriesSeries map[int64]int `json:"domains_over_time"` + BlockedQueries int `json:"ads_blocked_today"` + BlockedSeries map[int64]int `json:"ads_over_time"` + BlockedPercentage float64 `json:"ads_percentage_today"` + TopBlockedDomains piholeTopBlockedDomains `json:"top_ads"` + DomainsBlocked int `json:"domains_being_blocked"` } // If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array -// Use alternate struct without that field to avoid error when unmarshalling -type piholeStatsResponsePrivate struct { - TotalQueries int `json:"dns_queries_today"` - QueriesSeries map[int64]int `json:"domains_over_time"` - BlockedQueries int `json:"ads_blocked_today"` - BlockedSeries map[int64]int `json:"ads_over_time"` - BlockedPercentage float64 `json:"ads_percentage_today"` - DomainsBlocked int `json:"domains_being_blocked"` +// Use custom unmarshal behavior to avoid not getting the rest of the valid data when unmarshalling +type piholeTopBlockedDomains map[string]int + +func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error { + // NOTE: do not change to piholeTopBlockedDomains type here or it will cause a stack overflow + // because of the UnmarshalJSON method getting called recursively + temp := make(map[string]int) + + err := json.Unmarshal(data, &temp) + + if err != nil { + *p = make(piholeTopBlockedDomains) + } else { + *p = temp + } + + return nil } func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { @@ -42,27 +52,13 @@ func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { if err != nil { return nil, err } - + responseJson, err := decodeJsonFromRequest[piholeStatsResponse](defaultClient, request) - + if err != nil { - // Refer to piholeStatsResponsePrivate above - responseJsonPriv, err := - decodeJsonFromRequest[piholeStatsResponsePrivate](defaultClient, request) - if err != nil { - return nil, err - } - - // Copy the results back to responseJson, leaving the TopBlockedDomains field empty - responseJson.TotalQueries = responseJsonPriv.TotalQueries - responseJson.QueriesSeries = responseJsonPriv.QueriesSeries - responseJson.BlockedQueries = responseJsonPriv.BlockedQueries - responseJson.BlockedSeries = responseJsonPriv.BlockedSeries - responseJson.BlockedPercentage = responseJsonPriv.BlockedPercentage - responseJson.TopBlockedDomains = make(map[string]int) - responseJson.DomainsBlocked = responseJsonPriv.DomainsBlocked + return nil, err } - + stats := &DNSStats{ TotalQueries: responseJson.TotalQueries, BlockedQueries: responseJson.BlockedQueries, From d5fa6424c16ec3722bea3ca40cb16ea3e5b60b2e Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Thu, 12 Sep 2024 22:18:42 +0100 Subject: [PATCH 004/272] Fix bookmark icons shrinking when text wraps --- internal/assets/static/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/assets/static/main.css b/internal/assets/static/main.css index 03b64c7..d5ab9bb 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -1050,6 +1050,7 @@ details[open] .summary::after { border-radius: var(--border-radius); padding: 0.5rem; opacity: 0.7; + flex-shrink: 0; } .bookmarks-icon { From b0c4d70628369f55977aca741e320c6a7dcb9bfb Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Fri, 27 Sep 2024 21:04:48 +0100 Subject: [PATCH 005/272] Fix bug with collapsible grids inside of group widget Also move utils to new file --- internal/assets/static/js/main.js | 37 +++++++----------------------- internal/assets/static/js/utils.js | 25 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 internal/assets/static/js/utils.js diff --git a/internal/assets/static/js/main.js b/internal/assets/static/js/main.js index 228f57d..ffa7eb7 100644 --- a/internal/assets/static/js/main.js +++ b/internal/assets/static/js/main.js @@ -1,27 +1,5 @@ import { setupPopovers } from './popover.js'; - -function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { - let debounceTimeout; - let timesDebounced = 0; - - return function () { - if (timesDebounced == maxDebounceTimes) { - clearTimeout(debounceTimeout); - timesDebounced = 0; - callback(); - return; - } - - clearTimeout(debounceTimeout); - timesDebounced++; - - debounceTimeout = setTimeout(() => { - timesDebounced = 0; - callback(); - }, debounceDelay); - }; -}; - +import { throttledDebounce, isElementVisible } from './utils.js'; async function fetchPageContent(pageData) { // TODO: handle non 200 status codes/time outs @@ -427,7 +405,7 @@ function setupCollapsibleGrids() { const button = attachExpandToggleButton(gridElement); - let cardsPerRow = 2; + let cardsPerRow; const resolveCollapsibleItems = () => { const hideItemsAfterIndex = cardsPerRow * collapseAfterRows; @@ -457,12 +435,11 @@ function setupCollapsibleGrids() { } }; - afterContentReady(() => { - cardsPerRow = getCardsPerRow(); - resolveCollapsibleItems(); - }); + const observer = new ResizeObserver(() => { + if (!isElementVisible(gridElement)) { + return; + } - window.addEventListener("resize", () => { const newCardsPerRow = getCardsPerRow(); if (cardsPerRow == newCardsPerRow) { @@ -472,6 +449,8 @@ function setupCollapsibleGrids() { cardsPerRow = newCardsPerRow; resolveCollapsibleItems(); }); + + afterContentReady(() => observer.observe(gridElement)); } } diff --git a/internal/assets/static/js/utils.js b/internal/assets/static/js/utils.js new file mode 100644 index 0000000..af02086 --- /dev/null +++ b/internal/assets/static/js/utils.js @@ -0,0 +1,25 @@ +export function throttledDebounce(callback, maxDebounceTimes, debounceDelay) { + let debounceTimeout; + let timesDebounced = 0; + + return function () { + if (timesDebounced == maxDebounceTimes) { + clearTimeout(debounceTimeout); + timesDebounced = 0; + callback(); + return; + } + + clearTimeout(debounceTimeout); + timesDebounced++; + + debounceTimeout = setTimeout(() => { + timesDebounced = 0; + callback(); + }, debounceDelay); + }; +}; + +export function isElementVisible(element) { + return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length); +} From 9a36187333649ddd84d256e1b225a95456a25957 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:56:53 +0100 Subject: [PATCH 006/272] Add /api/healthz endpoint --- internal/glance/glance.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/glance/glance.go b/internal/glance/glance.go index d26745f..f47c66a 100644 --- a/internal/glance/glance.go +++ b/internal/glance/glance.go @@ -275,6 +275,9 @@ func (a *Application) Serve() error { mux.HandleFunc("GET /api/pages/{page}/content/{$}", a.HandlePageContentRequest) mux.HandleFunc("/api/widgets/{widget}/{path...}", a.HandleWidgetRequest) + mux.HandleFunc("GET /api/healthz", func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + }) mux.Handle( fmt.Sprintf("GET /static/%s/{path...}", a.Config.Server.AssetsHash), From 7e38ab624a8a92c64c9fdf31cf57800277392b3c Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 1 Oct 2024 08:59:11 +0200 Subject: [PATCH 007/272] Update configuration.md --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 116f8da..6f9d602 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -852,7 +852,7 @@ Either a value from the table below or a URL to a custom search engine. Use `{QU ##### `new-tab` When set to `true`, swaps the shortcuts for showing results in the same or new tab, defaulting to showing results in a new tab. -##### `new-tab` +##### `autofocus` When set to `true`, automatically focuses the search input on page load. ##### `bangs` From 4420d1df2c1a205c771c15b3ba0ad47efbfe4a15 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:47:56 +0100 Subject: [PATCH 008/272] Bump versions & update dependencies --- Dockerfile | 2 +- go.mod | 8 ++++---- go.sum | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index e4019ba..48f214b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22.5-alpine3.20 AS builder +FROM golang:1.23.1-alpine3.20 AS builder WORKDIR /app COPY . /app diff --git a/go.mod b/go.mod index 17aa4d4..6b39a2e 100644 --- a/go.mod +++ b/go.mod @@ -1,19 +1,19 @@ module github.com/glanceapp/glance -go 1.22.5 +go 1.23.1 require ( github.com/mmcdole/gofeed v1.3.0 - golang.org/x/text v0.16.0 + golang.org/x/text v0.18.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/PuerkitoBio/goquery v1.9.2 // indirect + github.com/PuerkitoBio/goquery v1.10.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.29.0 // indirect ) diff --git a/go.sum b/go.sum index 28cb1ae..ed770ea 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= +github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,6 +37,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -56,6 +60,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 8a8aaa752e5dc5ad2d1a478c0267dfab9732122b Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:40:25 +0100 Subject: [PATCH 009/272] Fix edge case in weather widget If the temperature for the entire day is the same the range is 0, divide by 0 no bueno --- internal/feed/openmeteo.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/feed/openmeteo.go b/internal/feed/openmeteo.go index 2a8dfa6..2bfa8f2 100644 --- a/internal/feed/openmeteo.go +++ b/internal/feed/openmeteo.go @@ -189,12 +189,19 @@ func FetchWeatherForPlace(place *PlaceJson, units string) (*Weather, error) { minT := slices.Min(temperatures) maxT := slices.Max(temperatures) + temperaturesRange := float64(maxT - minT) + for i := 0; i < 12; i++ { bars = append(bars, weatherColumn{ Temperature: temperatures[i], - Scale: float64(temperatures[i]-minT) / float64(maxT-minT), HasPrecipitation: precipitations[i], }) + + if temperaturesRange > 0 { + bars[i].Scale = float64(temperatures[i]-minT) / temperaturesRange + } else { + bars[i].Scale = 1 + } } } From e434fe08475e50c3166f6c1136c3519767ed74ce Mon Sep 17 00:00:00 2001 From: Eric Haughee Date: Thu, 3 Oct 2024 11:10:34 -0700 Subject: [PATCH 010/272] Add Markets sort-by: change to sort by percent change --- docs/configuration.md | 2 +- internal/feed/primitives.go | 6 ++++++ internal/widget/markets.go | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 116f8da..242e9ea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1539,7 +1539,7 @@ Preview: An array of markets for which to display information about. ##### `sort-by` -By default the markets are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `absolute-change` for descending order based on the stock's absolute price change. +By default the markets are displayed in the order they were defined. You can customize their ordering by setting the `sort-by` property to `change` for descending order based on the stock's percentage change (e.g. 1% would be sorted higher than -1%) or `absolute-change` for descending order based on the stock's absolute price change (e.g. -1% would be sorted higher than +0.5%). ###### Properties for each stock | Name | Type | Required | diff --git a/internal/feed/primitives.go b/internal/feed/primitives.go index 755b002..90a6a52 100644 --- a/internal/feed/primitives.go +++ b/internal/feed/primitives.go @@ -133,6 +133,12 @@ func (t Markets) SortByAbsChange() { }) } +func (t Markets) SortByChange() { + sort.Slice(t, func(i, j int) bool { + return t[i].PercentChange > t[j].PercentChange + }) +} + var weatherCodeTable = map[int]string{ 0: "Clear Sky", 1: "Mainly Clear", diff --git a/internal/widget/markets.go b/internal/widget/markets.go index 0d80973..27c431b 100644 --- a/internal/widget/markets.go +++ b/internal/widget/markets.go @@ -38,6 +38,10 @@ func (widget *Markets) Update(ctx context.Context) { markets.SortByAbsChange() } + if widget.Sort == "change" { + markets.SortByChange() + } + widget.Markets = markets } From ea3b8124fcb7367ed41395b26774abc69a0a94aa Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:08:52 +0100 Subject: [PATCH 011/272] Remove unused class --- internal/assets/templates/releases.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/assets/templates/releases.html b/internal/assets/templates/releases.html index 9bef5a0..7cd89f7 100644 --- a/internal/assets/templates/releases.html +++ b/internal/assets/templates/releases.html @@ -1,7 +1,7 @@ {{ template "widget-base.html" . }} {{ define "widget-content" }} -