Compare commits

...
25 Commits
Author SHA1 Message Date
Svilen Markov d7bbf2b8e2 Accessibility improvements
Create release / release (push) Has been cancelled
2025-03-12 18:01:35 +00:00
Svilen Markov b8df34309f Add note to docs about page width 2025-03-12 18:01:35 +00:00
Svilen Markov 278fa3c397 Add gap between domain and percent 2025-03-12 18:01:35 +00:00
Svilen Markov 6f48ee98e5 Uppercase currency before looking for matching symbol #365 2025-03-12 18:01:35 +00:00
Svilen Markov 774b0c104b Merge pull request #399 from KadoBOT/patch-1
Update configuration.md
2025-03-08 09:46:37 +00:00
Ricardo Ambrogi fbd4d9a74e Update configuration.md
Correct `title` to `name`.

I'm unsure if `title` is also accepted, I haven't tested it. Please close this PR if this fix is not valid.
2025-03-07 09:07:10 +01:00
Svilen Markov 3f2fefe4f7 Merge pull request #382 from legoraft/main
Added gruvbox theme
2025-03-01 22:44:22 +00:00
Legoraft fb8513bc9f Added gruvbox theme code and screenshot 2025-03-01 14:19:36 +01:00
Legoraft e4ec958edb Added gruvbox screenshot 2025-03-01 14:17:17 +01:00
Svilen Markov 488a1f6070 Update link to point to repo instead of pkg.go.dev 2025-02-22 13:32:50 +00:00
Svilen Markov 1e12d937aa Merge pull request #372 from DavisYe/main
Correct document
2025-02-22 12:58:56 +00:00
DavisYe 7319870289 Correct the docker compose environment format of GITHUB_TOKEN in the configuration document. 2025-02-20 21:30:34 +08:00
Svilen Markov d4565acfe7 Markets widget rate limit fix
Create release / release (push) Has been cancelled
2025-02-19 02:25:07 +00:00
Svilen Markov 16129c53bd Merge pull request #358 from rubiojr/rubiojr/reload-on-rename2
Auto-reload config file on RENAME
2025-02-19 01:25:08 +00:00
Svilen Markov cbf1961510 Don't try to get sensor info on openbsd 2025-02-17 23:48:16 +00:00
Svilen Markov c76a4d4be7 Increase docker containers widget timeout 2025-02-17 23:45:57 +00:00
Svilen Markov 27af0400c0 Tweak impl for handling config renames 2025-02-17 22:28:10 +00:00
Sergio Rubio 76a80ff034 Add clarifying comment 2025-02-17 19:17:49 +01:00
Sergio Rubio f7f333ad52 Auto-reload config file on RENAME
Some editors (like Vim), create a temp file when saving, then replace
the file being edited with the temp file. This causes the FS notify
event to be RENAME, not WRITE, so auto-reload misses this.

In addition to that, the file is removed from the watcher and the
auto-reload functionality stops working entirely after the first RENAME.

https://github.com/fsnotify/fsnotify/issues/255 describes this.
2025-02-17 19:08:54 +01:00
Svilen Markov 0ce45e32aa Update dashboard icons repo
Create release / release (push) Has been cancelled
2025-02-15 14:59:20 +00:00
Svilen Markov abeb11c8a6 Add hide-mountpoints-by-default prop for server-stats 2025-02-15 14:43:25 +00:00
Svilen Markov e01af4adec Delay changing popover display
Previously would make the popover visible and then reposition
it on the next frame in order to avoid getting called recursively
due to the observer, however this causes the scrollbar to appear
if it wasn't already visible for a single frame which is janky.

This change fixes that.
2025-02-15 13:53:13 +00:00
Svilen Markov 3043a0bd15 Rework getting host info (#340) 2025-02-15 13:46:58 +00:00
Svilen Markov 232cab01f8 Fix typo 2025-02-10 23:15:12 +00:00
Svilen Markov b301953249 Update docs 2025-02-10 20:38:50 +00:00
18 changed files with 183 additions and 69 deletions
+28 -10
View File
@@ -97,9 +97,9 @@ Including config files from within your main config file is supported. This is d
```yaml ```yaml
pages: pages:
!include home.yml !include: home.yml
!include videos.yml !include: videos.yml
!include homelab.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: 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: columns:
- size: full - size: full
widgets: widgets:
!include rss.yml !include: rss.yml
- name: News - name: News
columns: columns:
- size: full - size: full
widgets: widgets:
- type: group - type: group
widgets: widgets:
!include rss.yml !include: rss.yml
- type: reddit - type: reddit
subreddit: news subreddit: news
``` ```
@@ -356,7 +356,7 @@ pages:
### Properties ### Properties
| Name | Type | Required | Default | | Name | Type | Required | Default |
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| title | string | yes | | | name | string | yes | |
| slug | string | no | | | slug | string | no | |
| width | string | no | | | width | string | no | |
| center-vertically | boolean | no | false | | center-vertically | boolean | no | false |
@@ -374,7 +374,7 @@ The URL friendly version of the title which is used to access the page. For exam
#### `width` #### `width`
The maximum width of the page on desktop. Possible values are `slim` and `wide`. The maximum width of the page on desktop. Possible values are `slim` and `wide`.
* default: `1600px` * default: `1600px` (when no value is specified)
* slim: `1100px` * slim: `1100px`
* wide: `1920px` * wide: `1920px`
@@ -1311,7 +1311,7 @@ headers:
When set to `true`, removes the border and padding around the widget. When set to `true`, removes the border and padding around the widget.
##### `template` ##### `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://pkg.go.dev/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). 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).
### Extension ### 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). 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).
@@ -1608,7 +1608,7 @@ services:
glance: glance:
image: glanceapp/glance image: glanceapp/glance
environment: environment:
- GITHUB_TOKEN: <your token> - GITHUB_TOKEN=<your token>
``` ```
and then use it in your `glance.yml` like this: and then use it in your `glance.yml` like this:
@@ -1672,7 +1672,7 @@ For services with multiple containers you can specify a `glance.id` on the "main
<br> <br>
```yaml ```yaml
servies: services:
immich-server: immich-server:
image: ghcr.io/immich-app/immich-server image: ghcr.io/immich-app/immich-server
labels: labels:
@@ -1852,11 +1852,29 @@ Whether to hide the swap usage.
| Name | Type | Required | Default | | Name | Type | Required | Default |
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| cpu-temp-sensor | string | no | | | cpu-temp-sensor | string | no | |
| hide-mointpoints-by-default | boolean | no | false |
| mountpoints | map\[string\]object | no | | | mountpoints | map\[string\]object | no | |
###### `cpu-temp-sensor` ###### `cpu-temp-sensor`
The name of the sensor to use for the CPU temperature. When not provided the widget will attempt to find the correct one, if it fails to do so the temperature will not be displayed. To view the available sensors you can use `sensors` command. The name of the sensor to use for the CPU temperature. When not provided the widget will attempt to find the correct one, if it fails to do so the temperature will not be displayed. To view the available sensors you can use `sensors` command.
###### `hide-mountpoints-by-default`
If set to `true` you'll have to manually make each mountpoint visible by adding a `hide: false` property to it like so:
```yaml
- type: server-stats
servers:
- type: local
hide-mountpoints-by-default: true
mountpoints:
"/":
hide: false
"/mnt/data":
hide: false
```
This is useful if you're running Glance inside of a container which usually mounts a lot of irrelevant filesystems.
###### `mountpoints` ###### `mountpoints`
A map of mountpoints to display disk usage for. The key is the path to the mountpoint and the value is an object with optional properties. Example: A map of mountpoints to display disk usage for. The key is the path to the mountpoint and the value is an object with optional properties. Example:
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+10
View File
@@ -53,6 +53,16 @@ theme:
primary-color: 97 13 80 primary-color: 97 13 80
``` ```
### Gruvbox Dark
![screenshot](images/themes/gruvbox.png)
```yaml
theme:
background-color: 0 0 16
primary-color: 43 59 81
positive-color: 61 66 44
negative-color: 6 96 59
```
### Kanagawa Dark ### Kanagawa Dark
![screenshot](images/themes/kanagawa-dark.png) ![screenshot](images/themes/kanagawa-dark.png)
```yaml ```yaml
+2 -2
View File
@@ -152,9 +152,9 @@ func newCustomIconField(value string) customIconField {
} }
if prefix == "di" { if prefix == "di" {
field.URL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/" + ext + "/" + basename + "." + ext field.URL = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/" + ext + "/" + basename + "." + ext
} else { } else {
field.URL = "https://cdn.jsdelivr.net/gh/selfhst/icons@main/" + ext + "/" + basename + "." + ext field.URL = "https://cdn.jsdelivr.net/gh/selfhst/icons/" + ext + "/" + basename + "." + ext
} }
default: default:
field.URL = value field.URL = value
+36 -12
View File
@@ -242,7 +242,7 @@ func configFilesWatcher(
// needed for lastContents and lastIncludes because they get updated in multiple goroutines // needed for lastContents and lastIncludes because they get updated in multiple goroutines
mu := sync.Mutex{} mu := sync.Mutex{}
checkForContentChangesBeforeCallback := func() { parseAndCompareBeforeCallback := func() {
currentContents, currentIncludes, err := parseYAMLIncludes(mainFilePath) currentContents, currentIncludes, err := parseYAMLIncludes(mainFilePath)
if err != nil { if err != nil {
onErr(fmt.Errorf("parsing main file contents for comparison: %w", err)) onErr(fmt.Errorf("parsing main file contents for comparison: %w", err))
@@ -268,15 +268,22 @@ func configFilesWatcher(
const debounceDuration = 500 * time.Millisecond const debounceDuration = 500 * time.Millisecond
var debounceTimer *time.Timer var debounceTimer *time.Timer
debouncedCallback := func() { debouncedParseAndCompareBeforeCallback := func() {
if debounceTimer != nil { if debounceTimer != nil {
debounceTimer.Stop() debounceTimer.Stop()
debounceTimer.Reset(debounceDuration) debounceTimer.Reset(debounceDuration)
} else { } else {
debounceTimer = time.AfterFunc(debounceDuration, checkForContentChangesBeforeCallback) debounceTimer = time.AfterFunc(debounceDuration, parseAndCompareBeforeCallback)
} }
} }
deleteLastInclude := func(filePath string) {
mu.Lock()
defer mu.Unlock()
fileAbsPath, _ := filepath.Abs(filePath)
delete(lastIncludes, fileAbsPath)
}
go func() { go func() {
for { for {
select { select {
@@ -285,16 +292,33 @@ func configFilesWatcher(
return return
} }
if event.Has(fsnotify.Write) { if event.Has(fsnotify.Write) {
debouncedCallback() debouncedParseAndCompareBeforeCallback()
} else if event.Has(fsnotify.Remove) { } else if event.Has(fsnotify.Rename) {
func() { // on linux the file will no longer be watched after a rename, on windows
mu.Lock() // it will continue to be watched with the new name but we have no access to
defer mu.Unlock() // the new name in this event in order to stop watching it manually and match the
fileAbsPath, _ := filepath.Abs(event.Name) // behavior in linux, may lead to weird unintended behaviors on windows as we're
delete(lastIncludes, fileAbsPath) // only handling renames from linux's perspective
}() // see https://github.com/fsnotify/fsnotify/issues/255
debouncedCallback() // remove the old file from our manually tracked includes, calling
// debouncedParseAndCompareBeforeCallback will re-add it if it's still
// required after it triggers
deleteLastInclude(event.Name)
// wait for file to maybe get created again
// see https://github.com/glanceapp/glance/pull/358
for i := 0; i < 10; i++ {
if _, err := os.Stat(event.Name); err == nil {
break
}
time.Sleep(200 * time.Millisecond)
}
debouncedParseAndCompareBeforeCallback()
} else if event.Has(fsnotify.Remove) {
deleteLastInclude(event.Name)
debouncedParseAndCompareBeforeCallback()
} }
case err, isOpen := <-watcher.Errors: case err, isOpen := <-watcher.Errors:
if !isOpen { if !isOpen {
+1 -1
View File
@@ -68,7 +68,7 @@ func newApplication(config *config) (*application, error) {
for w := range column.Widgets { for w := range column.Widgets {
widget := column.Widgets[w] widget := column.Widgets[w]
app.widgetByID[widget.id()] = widget app.widgetByID[widget.GetID()] = widget
widget.setProviders(providers) widget.setProviders(providers)
} }
+5
View File
@@ -284,7 +284,9 @@ function setupGroups() {
for (let i = 0; i < titles.length; i++) { for (let i = 0; i < titles.length; i++) {
titles[i].classList.remove("widget-group-title-current"); titles[i].classList.remove("widget-group-title-current");
titles[i].setAttribute("aria-selected", "false");
tabs[i].classList.remove("widget-group-content-current"); tabs[i].classList.remove("widget-group-content-current");
tabs[i].setAttribute("aria-hidden", "true");
} }
if (current < t) { if (current < t) {
@@ -296,7 +298,9 @@ function setupGroups() {
current = t; current = t;
title.classList.add("widget-group-title-current"); title.classList.add("widget-group-title-current");
title.setAttribute("aria-selected", "true");
tabs[t].classList.add("widget-group-content-current"); tabs[t].classList.add("widget-group-content-current");
tabs[t].setAttribute("aria-hidden", "false");
}); });
} }
} }
@@ -670,6 +674,7 @@ async function setupPage() {
setupLazyImages(); setupLazyImages();
} finally { } finally {
pageElement.classList.add("content-ready"); pageElement.classList.add("content-ready");
pageElement.setAttribute("aria-busy", "false");
for (let i = 0; i < contentReadyCallbacks.length; i++) { for (let i = 0; i < contentReadyCallbacks.length; i++) {
contentReadyCallbacks[i](); contentReadyCallbacks[i]();
+2 -1
View File
@@ -98,7 +98,6 @@ function showPopover() {
} }
contentElement.style.maxWidth = contentMaxWidth; contentElement.style.maxWidth = contentMaxWidth;
containerElement.style.display = "block";
activeTarget.classList.add("popover-active"); activeTarget.classList.add("popover-active");
document.addEventListener("keydown", handleHidePopoverOnEscape); document.addEventListener("keydown", handleHidePopoverOnEscape);
window.addEventListener("resize", queueRepositionContainer); window.addEventListener("resize", queueRepositionContainer);
@@ -106,6 +105,8 @@ function showPopover() {
} }
function repositionContainer() { function repositionContainer() {
containerElement.style.display = "block";
const targetBounds = activeTarget.dataset.popoverAnchor !== undefined const targetBounds = activeTarget.dataset.popoverAnchor !== undefined
? activeTarget.querySelector(activeTarget.dataset.popoverAnchor).getBoundingClientRect() ? activeTarget.querySelector(activeTarget.dataset.popoverAnchor).getBoundingClientRect()
: activeTarget.getBoundingClientRect(); : activeTarget.getBoundingClientRect();
+17 -4
View File
@@ -110,7 +110,7 @@
.visited-indicator:not(.text-truncate)::after, .visited-indicator:not(.text-truncate)::after,
.visited-indicator.text-truncate::before, .visited-indicator.text-truncate::before,
.bookmarks-link:not(.bookmarks-link-no-arrow)::after { .bookmarks-link:not(.bookmarks-link-no-arrow)::after {
content: '↗'; content: '↗' / "";
margin-left: 0.5em; margin-left: 0.5em;
display: inline-block; display: inline-block;
position: relative; position: relative;
@@ -189,7 +189,7 @@
} }
.expand-toggle-button-icon::before { .expand-toggle-button-icon::before {
content: ''; content: '' / "";
font-size: 0.8rem; font-size: 0.8rem;
transform: rotate(90deg); transform: rotate(90deg);
line-height: 1; line-height: 1;
@@ -341,6 +341,19 @@ html, body, .body-content {
height: 100%; height: 100%;
} }
h1, h2, h3, h4, h5 {
font: inherit;
}
.visually-hidden {
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
a { a {
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
@@ -563,7 +576,7 @@ kbd:active {
} }
.summary::after { .summary::after {
content: "◀"; content: "◀" / "";
font-size: 1.2em; font-size: 1.2em;
position: absolute; position: absolute;
top: 0; top: 0;
@@ -822,7 +835,7 @@ details[open] .summary::after {
} }
.list-horizontal-text > *:not(:last-child)::after { .list-horizontal-text > *:not(:last-child)::after {
content: '•'; content: '•' / "";
color: var(--color-text-subdue); color: var(--color-text-subdue);
margin: 0 0.4rem; margin: 0 0.4rem;
position: relative; position: relative;
+1 -1
View File
@@ -76,7 +76,7 @@
<summary class="summary">Top blocked domains</summary> <summary class="summary">Top blocked domains</summary>
<ul class="list list-gap-4 list-with-transition size-h5"> <ul class="list list-gap-4 list-with-transition size-h5">
{{ range .Stats.TopBlockedDomains }} {{ range .Stats.TopBlockedDomains }}
<li class="flex justify-between"> <li class="flex justify-between gap-10">
<div class="text-truncate rtl">{{ .Domain }}</div> <div class="text-truncate rtl">{{ .Domain }}</div>
<div class="text-right" style="width: 4rem;"><span class="color-highlight">{{ .PercentBlocked }}</span>%</div> <div class="text-right" style="width: 4rem;"><span class="color-highlight">{{ .PercentBlocked }}</span>%</div>
</li> </li>
+9 -8
View File
@@ -4,17 +4,18 @@
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="widget-group-header"> <div class="widget-group-header">
<div class="widget-header gap-20"> <div class="widget-header gap-20" role="tablist">
{{ range $i, $widget := .Widgets }} {{- range $i, $widget := .Widgets }}
<button class="widget-group-title{{ if eq $i 0 }} widget-group-title-current{{ end }}"{{ if ne "" .TitleURL }} data-title-url="{{ .TitleURL }}"{{ end }}>{{ $widget.Title }}</button> <button class="widget-group-title{{ if eq $i 0 }} widget-group-title-current{{ end }}"{{ if ne "" .TitleURL }} data-title-url="{{ .TitleURL }}"{{ end }} aria-selected="{{ if eq $i 0 }}true{{ else }}false{{ end }}" arial-level="2" role="tab" aria-controls="widget-{{ .GetID }}-tabpanel-{{ $i }}" id="widget-{{ .GetID }}-tab-{{ $i }}">{{ $widget.Title }}</button>
{{ end }} {{- end }}
</div> </div>
</div> </div>
<div class="widget-group-contents"> <div class="widget-group-contents">
{{ range $i, $widget := .Widgets }} {{- range $i, $widget := .Widgets }}
<div class="widget-group-content{{ if eq $i 0 }} widget-group-content-current{{ end }}">{{ .Render }}</div> <div class="widget-group-content{{ if eq $i 0 }} widget-group-content-current{{ end }}" id="widget-{{ .GetID }}-tabpanel-{{ $i }}" role="tabpanel" aria-labelledby="widget-{{ .GetID }}-tab-{{ $i }}" aria-hidden="{{ if eq $i 0 }}false{{ else }}true{{ end }}">
{{ end }} {{- .Render -}}
</div>
{{- end }}
</div> </div>
{{ end }} {{ end }}
+11 -9
View File
@@ -25,7 +25,7 @@
{{ define "navigation-links" }} {{ define "navigation-links" }}
{{ range .App.Config.Pages }} {{ range .App.Config.Pages }}
<a href="{{ $.App.Config.Server.BaseURL }}/{{ .Slug }}" class="nav-item{{ if eq .Slug $.Page.Slug }} nav-item-current{{ end }}">{{ .Title }}</a> <a href="{{ $.App.Config.Server.BaseURL }}/{{ .Slug }}" class="nav-item{{ if eq .Slug $.Page.Slug }} nav-item-current{{ end }}"{{ if eq .Slug $.Page.Slug }} aria-current="page"{{ end }}>{{ .Title }}</a>
{{ end }} {{ end }}
{{ end }} {{ end }}
@@ -35,10 +35,10 @@
<div class="header-container content-bounds"> <div class="header-container content-bounds">
<div class="header flex padding-inline-widget widget-content-frame"> <div class="header flex padding-inline-widget widget-content-frame">
<!-- TODO: Replace G with actual logo, first need an actual logo --> <!-- TODO: Replace G with actual logo, first need an actual logo -->
<div class="logo">{{ if ne "" .App.Config.Branding.LogoURL }}<img src="{{ .App.Config.Branding.LogoURL }}" alt="">{{ else if ne "" .App.Config.Branding.LogoText }}{{ .App.Config.Branding.LogoText }}{{ else }}G{{ end }}</div> <div class="logo" aria-hidden="true">{{ if ne "" .App.Config.Branding.LogoURL }}<img src="{{ .App.Config.Branding.LogoURL }}" alt="">{{ else if ne "" .App.Config.Branding.LogoText }}{{ .App.Config.Branding.LogoText }}{{ else }}G{{ end }}</div>
<div class="nav flex grow"> <nav class="nav flex grow">
{{ template "navigation-links" . }} {{ template "navigation-links" . }}
</div> </nav>
</div> </div>
</div> </div>
{{ end }} {{ end }}
@@ -57,17 +57,19 @@
</div> </div>
<div class="content-bounds grow"> <div class="content-bounds grow">
<div class="page" id="page"> <main class="page" id="page" aria-live="polite" aria-busy="true">
<h1 class="visually-hidden">{{ .Page.Title }}</h1>
<div class="page-content" id="page-content"></div> <div class="page-content" id="page-content"></div>
<div class="page-loading-container"> <div class="page-loading-container">
<!-- TODO: add a bigger/better loading indicator --> <!-- TODO: add a bigger/better loading indicator -->
<div class="loading-icon"></div> <div class="visually-hidden">Loading</div>
<div class="loading-icon" aria-hidden="true"></div>
</div> </div>
</div> </main>
</div> </div>
{{ if not .App.Config.Branding.HideFooter }} {{ if not .App.Config.Branding.HideFooter }}
<div class="footer flex items-center flex-column"> <footer class="footer flex items-center flex-column">
{{ if eq "" .App.Config.Branding.CustomFooter }} {{ if eq "" .App.Config.Branding.CustomFooter }}
<div> <div>
<a class="size-h3" href="https://github.com/glanceapp/glance" target="_blank" rel="noreferrer">Glance</a> {{ if ne "dev" .App.Version }}<a class="visited-indicator" title="Release notes" href="https://github.com/glanceapp/glance/releases/tag/{{ .App.Version }}" target="_blank" rel="noreferrer">{{ .App.Version }}</a>{{ else }}({{ .App.Version }}){{ end }} <a class="size-h3" href="https://github.com/glanceapp/glance" target="_blank" rel="noreferrer">Glance</a> {{ if ne "dev" .App.Version }}<a class="visited-indicator" title="Release notes" href="https://github.com/glanceapp/glance/releases/tag/{{ .App.Version }}" target="_blank" rel="noreferrer">{{ .App.Version }}</a>{{ else }}({{ .App.Version }}){{ end }}
@@ -75,7 +77,7 @@
{{ else }} {{ else }}
{{ .App.Config.Branding.CustomFooter }} {{ .App.Config.Branding.CustomFooter }}
{{ end }} {{ end }}
</div> </footer>
{{ end }} {{ end }}
<div class="mobile-navigation-offset"></div> <div class="mobile-navigation-offset"></div>
+2 -2
View File
@@ -2,9 +2,9 @@
{{- if not .HideHeader}} {{- if not .HideHeader}}
<div class="widget-header"> <div class="widget-header">
{{- if ne "" .TitleURL }} {{- if ne "" .TitleURL }}
<a href="{{ .TitleURL | safeURL }}" target="_blank" rel="noreferrer" class="uppercase">{{ .Title }}</a> <h2><a href="{{ .TitleURL | safeURL }}" target="_blank" rel="noreferrer" class="uppercase">{{ .Title }}</a></h2>
{{- else }} {{- else }}
<div class="uppercase">{{ .Title }}</div> <h2 class="uppercase">{{ .Title }}</h2>
{{- end }} {{- end }}
{{- if .IsWIP }} {{- if .IsWIP }}
<div data-popover-type="html" data-popover-position="above"> <div data-popover-type="html" data-popover-position="above">
+1 -1
View File
@@ -241,7 +241,7 @@ func isDockerContainerHidden(container *dockerContainerJsonResponse, hideByDefau
func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) { func fetchAllDockerContainersFromSock(socketPath string) ([]dockerContainerJsonResponse, error) {
client := &http.Client{ client := &http.Client{
Timeout: 3 * time.Second, Timeout: 5 * time.Second,
Transport: &http.Transport{ Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath) return net.Dial("unix", socketPath)
+2 -2
View File
@@ -124,6 +124,7 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
for i := range marketRequests { for i := range marketRequests {
request, _ := http.NewRequest("GET", fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?range=1mo&interval=1d", marketRequests[i].Symbol), nil) request, _ := http.NewRequest("GET", fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s?range=1mo&interval=1d", marketRequests[i].Symbol), nil)
setBrowserUserAgentHeader(request)
requests = append(requests, request) requests = append(requests, request)
} }
@@ -165,8 +166,7 @@ func fetchMarketsDataFromYahoo(marketRequests []marketRequest) (marketList, erro
points := svgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices)) points := svgPolylineCoordsFromYValues(100, 50, maybeCopySliceWithoutZeroValues(prices))
currency, exists := currencyToSymbol[response.Chart.Result[0].Meta.Currency] currency, exists := currencyToSymbol[strings.ToUpper(response.Chart.Result[0].Meta.Currency)]
if !exists { if !exists {
currency = response.Chart.Result[0].Meta.Currency currency = response.Chart.Result[0].Meta.Currency
} }
+11 -1
View File
@@ -8,8 +8,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math/rand/v2"
"net/http" "net/http"
"strconv"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@@ -35,8 +38,15 @@ type requestDoer interface {
Do(*http.Request) (*http.Response, error) Do(*http.Request) (*http.Response, error)
} }
var userAgentPersistentVersion atomic.Int32
func setBrowserUserAgentHeader(request *http.Request) { func setBrowserUserAgentHeader(request *http.Request) {
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0") if rand.IntN(2000) == 0 {
userAgentPersistentVersion.Store(rand.Int32N(5))
}
version := strconv.Itoa(130 + int(userAgentPersistentVersion.Load()))
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:"+version+".0) Gecko/20100101 Firefox/"+version+".0")
} }
func decodeJsonFromRequest[T any](client requestDoer, request *http.Request) (T, error) { func decodeJsonFromRequest[T any](client requestDoer, request *http.Request) (T, error) {
+2 -2
View File
@@ -121,13 +121,13 @@ type widget interface {
// These need to be exported because they get called in templates // These need to be exported because they get called in templates
Render() template.HTML Render() template.HTML
GetType() string GetType() string
GetID() uint64
initialize() error initialize() error
requiresUpdate(*time.Time) bool requiresUpdate(*time.Time) bool
setProviders(*widgetProviders) setProviders(*widgetProviders)
update(context.Context) update(context.Context)
setID(uint64) setID(uint64)
id() uint64
handleRequest(w http.ResponseWriter, r *http.Request) handleRequest(w http.ResponseWriter, r *http.Request)
setHideHeader(bool) setHideHeader(bool)
} }
@@ -184,7 +184,7 @@ func (w *widgetBase) update(ctx context.Context) {
} }
func (w *widgetBase) id() uint64 { func (w *widgetBase) GetID() uint64 {
return w.ID return w.ID
} }
+43 -13
View File
@@ -3,6 +3,7 @@ package sysinfo
import ( import (
"fmt" "fmt"
"math" "math"
"os"
"runtime" "runtime"
"sort" "sort"
"strconv" "strconv"
@@ -73,24 +74,52 @@ type MountpointInfo struct {
} }
type SystemInfoRequest struct { type SystemInfoRequest struct {
CPUTempSensor string `yaml:"cpu-temp-sensor"` CPUTempSensor string `yaml:"cpu-temp-sensor"`
Mountpoints map[string]MointpointRequest `yaml:"mountpoints"` HideMountpointsByDefault bool `yaml:"hide-mountpoints-by-default"`
Mountpoints map[string]MointpointRequest `yaml:"mountpoints"`
} }
type MointpointRequest struct { type MointpointRequest struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Hide bool `yaml:"hide"` Hide *bool `yaml:"hide"`
} }
// Currently caches hostname indefinitely which isn't ideal // Currently caches hostname indefinitely which isn't ideal
// Potential issue with caching boot time as it may not initially get reported correctly: // Potential issue with caching boot time as it may not initially get reported correctly:
// https://github.com/shirou/gopsutil/issues/842#issuecomment-1908972344 // https://github.com/shirou/gopsutil/issues/842#issuecomment-1908972344
var cachedHostInfo = struct { type cacheableHostInfo struct {
available bool available bool
hostname string hostname string
platform string platform string
bootTime timestampJSON bootTime timestampJSON
}{} }
var cachedHostInfo cacheableHostInfo
func getHostInfo() (cacheableHostInfo, error) {
var err error
info := cacheableHostInfo{}
info.hostname, err = os.Hostname()
if err != nil {
return info, err
}
info.platform, _, _, err = host.PlatformInformation()
if err != nil {
return info, err
}
bootTime, err := host.BootTime()
if err != nil {
return info, err
}
info.bootTime = timestampJSON{time.Unix(int64(bootTime), 0)}
info.available = true
return info, nil
}
func Collect(req *SystemInfoRequest) (*SystemInfo, []error) { func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
if req == nil { if req == nil {
@@ -117,13 +146,9 @@ func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
if cachedHostInfo.available { if cachedHostInfo.available {
applyCachedHostInfo() applyCachedHostInfo()
} else { } else {
hostInfo, err := host.Info() hostInfo, err := getHostInfo()
if err == nil { if err == nil {
cachedHostInfo.available = true cachedHostInfo = hostInfo
cachedHostInfo.bootTime = timestampJSON{time.Unix(int64(hostInfo.BootTime), 0)}
cachedHostInfo.hostname = hostInfo.Hostname
cachedHostInfo.platform = hostInfo.Platform
applyCachedHostInfo() applyCachedHostInfo()
} else { } else {
addErr(fmt.Errorf("getting host info: %v", err)) addErr(fmt.Errorf("getting host info: %v", err))
@@ -177,7 +202,8 @@ func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
// keeps returning a single sensor with key "ACPI\\ThermalZone\\TZ00_0" which // keeps returning a single sensor with key "ACPI\\ThermalZone\\TZ00_0" which
// doesn't seem to be the CPU sensor or correspond to anything useful when // doesn't seem to be the CPU sensor or correspond to anything useful when
// compared against the temperatures Libre Hardware Monitor reports // compared against the temperatures Libre Hardware Monitor reports
if runtime.GOOS != "windows" { // also disabled on openbsd because it's not implemented by go-psutil
if runtime.GOOS != "windows" && runtime.GOOS != "openbsd" {
sensorReadings, err := sensors.SensorsTemperatures() sensorReadings, err := sensors.SensorsTemperatures()
if err == nil { if err == nil {
if req.CPUTempSensor != "" { if req.CPUTempSensor != "" {
@@ -205,7 +231,11 @@ func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
if err == nil { if err == nil {
for _, fs := range filesystems { for _, fs := range filesystems {
mpReq, ok := req.Mountpoints[fs.Mountpoint] mpReq, ok := req.Mountpoints[fs.Mountpoint]
if ok && mpReq.Hide { isHidden := req.HideMountpointsByDefault
if ok && mpReq.Hide != nil {
isHidden = *mpReq.Hide
}
if isHidden {
continue continue
} }