Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4565acfe7 | ||
|
|
16129c53bd | ||
|
|
cbf1961510 | ||
|
|
c76a4d4be7 | ||
|
|
27af0400c0 | ||
|
|
76a80ff034 | ||
|
|
f7f333ad52 | ||
|
|
0ce45e32aa | ||
|
|
abeb11c8a6 | ||
|
|
e01af4adec | ||
|
|
3043a0bd15 | ||
|
|
232cab01f8 | ||
|
|
b301953249 | ||
|
|
bb9cb03c8a |
+24
-6
@@ -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
|
||||||
```
|
```
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ func (widget *videosWidget) initialize() error {
|
|||||||
// playlists are separate things rather than specifying a list of channels and some of
|
// playlists are separate things rather than specifying a list of channels and some of
|
||||||
// them awkwardly have a "playlist:" prefix
|
// them awkwardly have a "playlist:" prefix
|
||||||
if len(widget.Playlists) > 0 {
|
if len(widget.Playlists) > 0 {
|
||||||
|
initialLen := len(widget.Channels)
|
||||||
widget.Channels = append(widget.Channels, make([]string, len(widget.Playlists))...)
|
widget.Channels = append(widget.Channels, make([]string, len(widget.Playlists))...)
|
||||||
|
|
||||||
for i := range widget.Playlists {
|
for i := range widget.Playlists {
|
||||||
widget.Channels[len(widget.Channels)-1+i] = "playlist:" + widget.Playlists[i]
|
widget.Channels[initialLen+i] = "playlist:" + widget.Playlists[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-13
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user