From 04c3afc850803e4bdfc8f4914a49602d0385974f Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 8 Sep 2024 06:25:06 +0100 Subject: [PATCH 001/279] Document `center-vertically` property --- docs/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index d39c8b5..944019e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -307,6 +307,7 @@ pages: | title | string | yes | | | slug | string | no | | | width | string | no | | +| center-vertically | boolean | no | false | | hide-desktop-navigation | boolean | no | false | | show-mobile-header | boolean | no | false | | columns | array | yes | | @@ -328,6 +329,8 @@ The maximum width of the page on desktop. Possible values are `slim` and `wide`. > > When using `slim`, the maximum number of columns allowed for that page is `2`. +#### `center-vertically` +When set to `true`, vertically centers the content on the page. Has no effect if the content is taller than the height of the viewport. #### `hide-desktop-navigation` Whether to show the navigation links at the top of the page on desktop. From 60f4183057f7091fc0290e04ba3be02d3c0aa388 Mon Sep 17 00:00:00 2001 From: micash Date: Sun, 8 Sep 2024 16:59:24 +0200 Subject: [PATCH 002/279] add codeberg releases --- docs/configuration.md | 8 +++-- internal/assets/static/icons/codeberg.svg | 1 + internal/feed/codeberg.go | 39 +++++++++++++++++++++++ internal/feed/releases.go | 3 ++ internal/widget/releases.go | 6 +++- 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 internal/assets/static/icons/codeberg.svg create mode 100644 internal/feed/codeberg.go diff --git a/docs/configuration.md b/docs/configuration.md index 944019e..fb119a5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1114,7 +1114,7 @@ Whether to ignore invalid/self-signed certificates. Whether to open the link in the same or a new tab. ### Releases -Display a list of latest releases for specific repositories on Github, GitLab or Docker Hub. +Display a list of latest releases for specific repositories on Github, GitLab, Codeberg or Docker Hub. Example: @@ -1125,6 +1125,7 @@ Example: - go-gitea/gitea - jellyfin/jellyfin - glanceapp/glance + - codeberg:redict/redict - gitlab:fdroid/fdroidclient - dockerhub:gotify/server ``` @@ -1145,12 +1146,13 @@ Preview: | collapse-after | integer | no | 5 | ##### `repositories` -A list of repositores to fetch the latest release for. Only the name/repo is required, not the full URL. A prefix can be specified for repositories hosted elsewhere such as GitLab and Docker Hub. Example: +A list of repositores to fetch the latest release for. Only the name/repo is required, not the full URL. A prefix can be specified for repositories hosted elsewhere such as GitLab, Codeberg and Docker Hub. Example: ```yaml repositories: - gitlab:inkscape/inkscape - dockerhub:glanceapp/glance + - codeberg:redict/redict ``` Official images on Docker Hub can be specified by ommiting the owner: @@ -1172,7 +1174,7 @@ repositories: ##### `show-source-icon` -Shows an icon of the source (GitHub/GitLab/Docker Hub) next to the repository name when set to `true`. +Shows an icon of the source (GitHub/GitLab/Codeberg/Docker Hub) next to the repository name when set to `true`. ##### `token` Without authentication Github allows for up to 60 requests per hour. You can easily exceed this limit and start seeing errors if you're tracking lots of repositories or your cache time is low. To circumvent this you can [create a read only token from your Github account](https://github.com/settings/personal-access-tokens/new) and provide it here. diff --git a/internal/assets/static/icons/codeberg.svg b/internal/assets/static/icons/codeberg.svg new file mode 100644 index 0000000..a87790e --- /dev/null +++ b/internal/assets/static/icons/codeberg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/internal/feed/codeberg.go b/internal/feed/codeberg.go new file mode 100644 index 0000000..edd83d7 --- /dev/null +++ b/internal/feed/codeberg.go @@ -0,0 +1,39 @@ +package feed + +import ( + "fmt" + "net/http" +) + +type codebergReleaseResponseJson struct { + TagName string `json:"tag_name"` + PublishedAt string `json:"published_at"` + HtmlUrl string `json:"html_url"` +} + +func FetchLatestCodebergRelease(request *ReleaseRequest) (*AppRelease, error) { + httpRequest, err := http.NewRequest( + "GET", + fmt.Sprintf( + "https://codeberg.org/api/v1/repos/%s/releases/latest", + request.Repository, + ), + nil, + ) + if err != nil { + return nil, err + } + + response, err := decodeJsonFromRequest[codebergReleaseResponseJson](defaultClient, httpRequest) + + if err != nil { + return nil, err + } + return &AppRelease{ + Source: ReleaseSourceCodeberg, + Name: request.Repository, + Version: normalizeVersionFormat(response.TagName), + NotesUrl: response.HtmlUrl, + TimeReleased: parseRFC3339Time(response.PublishedAt), + }, nil +} diff --git a/internal/feed/releases.go b/internal/feed/releases.go index 516801e..596d1be 100644 --- a/internal/feed/releases.go +++ b/internal/feed/releases.go @@ -9,6 +9,7 @@ import ( type ReleaseSource string const ( + ReleaseSourceCodeberg ReleaseSource = "codeberg" ReleaseSourceGithub ReleaseSource = "github" ReleaseSourceGitlab ReleaseSource = "gitlab" ReleaseSourceDockerHub ReleaseSource = "dockerhub" @@ -57,6 +58,8 @@ func FetchLatestReleases(requests []*ReleaseRequest) (AppReleases, error) { func fetchLatestReleaseTask(request *ReleaseRequest) (*AppRelease, error) { switch request.Source { + case ReleaseSourceCodeberg: + return FetchLatestCodebergRelease(request) case ReleaseSourceGithub: return fetchLatestGithubRelease(request) case ReleaseSourceGitlab: diff --git a/internal/widget/releases.go b/internal/widget/releases.go index c7831cb..74b5af7 100644 --- a/internal/widget/releases.go +++ b/internal/widget/releases.go @@ -40,7 +40,6 @@ func (widget *Releases) Initialize() error { for _, repository := range widget.Repositories { parts := strings.SplitN(repository, ":", 2) var request *feed.ReleaseRequest - if len(parts) == 1 { request = &feed.ReleaseRequest{ Source: feed.ReleaseSourceGithub, @@ -65,6 +64,11 @@ func (widget *Releases) Initialize() error { Source: feed.ReleaseSourceDockerHub, Repository: parts[1], } + } else if parts[0] == string(feed.ReleaseSourceCodeberg) { + request = &feed.ReleaseRequest{ + Source: feed.ReleaseSourceCodeberg, + Repository: parts[1], + } } else { return errors.New("invalid repository source " + parts[0]) } From 1a05d6d03a04d165ef5633aae668de3a62539245 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:34:43 +0100 Subject: [PATCH 003/279] Don't export function --- internal/feed/codeberg.go | 2 +- internal/feed/releases.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/feed/codeberg.go b/internal/feed/codeberg.go index edd83d7..d5e7b7c 100644 --- a/internal/feed/codeberg.go +++ b/internal/feed/codeberg.go @@ -11,7 +11,7 @@ type codebergReleaseResponseJson struct { HtmlUrl string `json:"html_url"` } -func FetchLatestCodebergRelease(request *ReleaseRequest) (*AppRelease, error) { +func fetchLatestCodebergRelease(request *ReleaseRequest) (*AppRelease, error) { httpRequest, err := http.NewRequest( "GET", fmt.Sprintf( diff --git a/internal/feed/releases.go b/internal/feed/releases.go index 596d1be..b0cdc25 100644 --- a/internal/feed/releases.go +++ b/internal/feed/releases.go @@ -59,7 +59,7 @@ func FetchLatestReleases(requests []*ReleaseRequest) (AppReleases, error) { func fetchLatestReleaseTask(request *ReleaseRequest) (*AppRelease, error) { switch request.Source { case ReleaseSourceCodeberg: - return FetchLatestCodebergRelease(request) + return fetchLatestCodebergRelease(request) case ReleaseSourceGithub: return fetchLatestGithubRelease(request) case ReleaseSourceGitlab: From a68a907fa727984c35729aab60e133061e5f113e Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:35:01 +0100 Subject: [PATCH 004/279] Update SVG --- internal/assets/static/icons/codeberg.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/assets/static/icons/codeberg.svg b/internal/assets/static/icons/codeberg.svg index a87790e..3bb3c9f 100644 --- a/internal/assets/static/icons/codeberg.svg +++ b/internal/assets/static/icons/codeberg.svg @@ -1 +1 @@ - \ No newline at end of file + From bd9abf2e52ce07d54ef147d608aa21bad209308b Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:36:11 +0100 Subject: [PATCH 005/279] Fix mobile nav links wrapping --- 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 c2a4acd..03b64c7 100644 --- a/internal/assets/static/main.css +++ b/internal/assets/static/main.css @@ -851,6 +851,7 @@ details[open] .summary::after { border-bottom: 2px solid transparent; transition: color .3s, border-color .3s; font-size: var(--font-size-h3); + flex-shrink: 0; } .nav-item:not(.nav-item-current):hover { From adef35049f6185aba68fe4b694c855b0af8191e3 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Tue, 10 Sep 2024 05:33:32 +0100 Subject: [PATCH 006/279] Add warning when pihole doesn't return expected data points --- internal/feed/pihole.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/feed/pihole.go b/internal/feed/pihole.go index badfb1a..3849267 100644 --- a/internal/feed/pihole.go +++ b/internal/feed/pihole.go @@ -2,6 +2,7 @@ package feed import ( "errors" + "log/slog" "net/http" "sort" "strings" @@ -63,6 +64,11 @@ func FetchPiholeStats(instanceURL, token string) (*DNSStats, error) { // Pihole _should_ return data for the last 24 hours in a 10 minute interval, 6*24 = 144 if len(responseJson.QueriesSeries) != 144 || len(responseJson.BlockedSeries) != 144 { + slog.Warn( + "DNS stats for pihole: did not get expected 144 data points", + "len(queries)", len(responseJson.QueriesSeries), + "len(blocked)", len(responseJson.BlockedSeries), + ) return stats, nil } From db1ed9e257d2df866ee34d42c4c7f080aa58795d Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Tue, 10 Sep 2024 05:34:56 +0100 Subject: [PATCH 007/279] Fix & refactor adguard stats --- internal/feed/adguard.go | 57 +++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/internal/feed/adguard.go b/internal/feed/adguard.go index 440cb88..87182c3 100644 --- a/internal/feed/adguard.go +++ b/internal/feed/adguard.go @@ -31,10 +31,13 @@ func FetchAdguardStats(instanceURL, username, password string) (*DNSStats, error return nil, err } + var topBlockedDomainsCount = min(len(responseJson.TopBlockedDomains), 5) + stats := &DNSStats{ - TotalQueries: responseJson.TotalQueries, - BlockedQueries: responseJson.BlockedQueries, - ResponseTime: int(responseJson.ResponseTime * 1000), + TotalQueries: responseJson.TotalQueries, + BlockedQueries: responseJson.BlockedQueries, + ResponseTime: int(responseJson.ResponseTime * 1000), + TopBlockedDomains: make([]DNSStatsBlockedDomain, 0, topBlockedDomainsCount), } if stats.TotalQueries <= 0 { @@ -43,8 +46,6 @@ func FetchAdguardStats(instanceURL, username, password string) (*DNSStats, error stats.BlockedPercent = int(float64(responseJson.BlockedQueries) / float64(responseJson.TotalQueries) * 100) - var topBlockedDomainsCount = min(len(responseJson.TopBlockedDomains), 5) - for i := 0; i < topBlockedDomainsCount; i++ { domain := responseJson.TopBlockedDomains[i] var firstDomain string @@ -59,31 +60,51 @@ func FetchAdguardStats(instanceURL, username, password string) (*DNSStats, error } stats.TopBlockedDomains = append(stats.TopBlockedDomains, DNSStatsBlockedDomain{ - Domain: firstDomain, - PercentBlocked: int(float64(domain[firstDomain]) / float64(responseJson.BlockedQueries) * 100), + Domain: firstDomain, }) + + if stats.BlockedQueries > 0 { + stats.TopBlockedDomains[i].PercentBlocked = int(float64(domain[firstDomain]) / float64(responseJson.BlockedQueries) * 100) + } } - // Adguard _should_ return data for the last 24 hours in a 1 hour interval - if len(responseJson.QueriesSeries) != 24 || len(responseJson.BlockedSeries) != 24 { - return stats, nil + 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(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 < 8; i++ { + for i := 0; i < bars; i++ { queries := 0 blocked := 0 - for j := 0; j < 3; j++ { - queries += responseJson.QueriesSeries[i*3+j] - blocked += responseJson.BlockedSeries[i*3+j] + for j := 0; j < hoursPerBar; j++ { + queries += queriesSeries[i*hoursPerBar+j] + blocked += blockedSeries[i*hoursPerBar+j] } stats.Series[i] = DNSStatsSeries{ - Queries: queries, - Blocked: blocked, - PercentBlocked: int(float64(blocked) / float64(queries) * 100), + Queries: queries, + Blocked: blocked, + } + + if queries > 0 { + stats.Series[i].PercentBlocked = int(float64(blocked) / float64(queries) * 100) } if queries > maxQueriesInSeries { @@ -91,7 +112,7 @@ func FetchAdguardStats(instanceURL, username, password string) (*DNSStats, error } } - for i := 0; i < 8; i++ { + for i := 0; i < bars; i++ { stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) } 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 008/279] 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 009/279] 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 010/279] 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 011/279] 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 012/279] 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 013/279] 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 014/279] 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 015/279] 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 016/279] 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 017/279] 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 018/279] 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" }} -