Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
|
03b616622e | ||
|
50e1da01fe | ||
|
f8f50b26d8 | ||
|
a3bc133bcb | ||
|
50dae22ff4 | ||
|
5a91154eab | ||
|
d5b89d512a | ||
|
97f43f88eb | ||
|
3177af9524 | ||
|
f99d22738c | ||
|
ea7124db52 | ||
|
b1247e5de6 | ||
|
5ab962634e | ||
|
d99944dcff | ||
|
920bcb06dc |
11 changed files with 123 additions and 30 deletions
|
@ -195,6 +195,7 @@ services:
|
||||||
glance:
|
glance:
|
||||||
container_name: glance
|
container_name: glance
|
||||||
image: glanceapp/glance
|
image: glanceapp/glance
|
||||||
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -389,6 +389,7 @@ The following helper functions provided by Glance are available:
|
||||||
- `trimSuffix(suffix string, str string) string`: Trims the suffix from a string.
|
- `trimSuffix(suffix string, str string) string`: Trims the suffix from a string.
|
||||||
- `trimSpace(str string) string`: Trims whitespace from a string on both ends.
|
- `trimSpace(str string) string`: Trims whitespace from a string on both ends.
|
||||||
- `replaceAll(old string, new string, str string) string`: Replaces all occurrences of a string in a string.
|
- `replaceAll(old string, new string, str string) string`: Replaces all occurrences of a string in a string.
|
||||||
|
- `replaceMatches(pattern string, replacement string, str string) string`: Replaces all occurrences of a regular expression in a string.
|
||||||
- `findMatch(pattern string, str string) string`: Finds the first match of a regular expression in a string.
|
- `findMatch(pattern string, str string) string`: Finds the first match of a regular expression in a string.
|
||||||
- `findSubmatch(pattern string, str string) string`: Finds the first submatch of a regular expression in a string.
|
- `findSubmatch(pattern string, str string) string`: Finds the first submatch of a regular expression in a string.
|
||||||
- `sortByString(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a string key in either ascending or descending order.
|
- `sortByString(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a string key in either ascending or descending order.
|
||||||
|
@ -396,6 +397,7 @@ The following helper functions provided by Glance are available:
|
||||||
- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order.
|
- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order.
|
||||||
- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants).
|
- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants).
|
||||||
- `concat(strings ...string) string`: Concatenates multiple strings together.
|
- `concat(strings ...string) string`: Concatenates multiple strings together.
|
||||||
|
- `unique(key string, arr []JSON) []JSON`: Returns a unique array of JSON objects based on the given key.
|
||||||
|
|
||||||
The following helper functions provided by Go's `text/template` are available:
|
The following helper functions provided by Go's `text/template` are available:
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
"github.com/shirou/gopsutil/v4/sensors"
|
"github.com/shirou/gopsutil/v4/sensors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,11 +19,13 @@ const (
|
||||||
cliIntentConfigPrint
|
cliIntentConfigPrint
|
||||||
cliIntentDiagnose
|
cliIntentDiagnose
|
||||||
cliIntentSensorsPrint
|
cliIntentSensorsPrint
|
||||||
|
cliIntentMountpointInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
type cliOptions struct {
|
type cliOptions struct {
|
||||||
intent cliIntent
|
intent cliIntent
|
||||||
configPath string
|
configPath string
|
||||||
|
args []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCliOptions() (*cliOptions, error) {
|
func parseCliOptions() (*cliOptions, error) {
|
||||||
|
@ -46,6 +49,7 @@ func parseCliOptions() (*cliOptions, error) {
|
||||||
fmt.Println(" config:validate Validate the config file")
|
fmt.Println(" config:validate Validate the config file")
|
||||||
fmt.Println(" config:print Print the parsed config file with embedded includes")
|
fmt.Println(" config:print Print the parsed config file with embedded includes")
|
||||||
fmt.Println(" sensors:print List all sensors")
|
fmt.Println(" sensors:print List all sensors")
|
||||||
|
fmt.Println(" mountpoint:info Print information about a given mountpoint path")
|
||||||
fmt.Println(" diagnose Run diagnostic checks")
|
fmt.Println(" diagnose Run diagnostic checks")
|
||||||
}
|
}
|
||||||
configPath := flags.String("config", "glance.yml", "Set config path")
|
configPath := flags.String("config", "glance.yml", "Set config path")
|
||||||
|
@ -72,6 +76,12 @@ func parseCliOptions() (*cliOptions, error) {
|
||||||
} else {
|
} else {
|
||||||
return nil, unknownCommandErr
|
return nil, unknownCommandErr
|
||||||
}
|
}
|
||||||
|
} else if len(args) == 2 {
|
||||||
|
if args[0] == "mountpoint:info" {
|
||||||
|
intent = cliIntentMountpointInfo
|
||||||
|
} else {
|
||||||
|
return nil, unknownCommandErr
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return nil, unknownCommandErr
|
return nil, unknownCommandErr
|
||||||
}
|
}
|
||||||
|
@ -79,6 +89,7 @@ func parseCliOptions() (*cliOptions, error) {
|
||||||
return &cliOptions{
|
return &cliOptions{
|
||||||
intent: intent,
|
intent: intent,
|
||||||
configPath: *configPath,
|
configPath: *configPath,
|
||||||
|
args: args,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,6 +97,12 @@ func cliSensorsPrint() int {
|
||||||
tempSensors, err := sensors.SensorsTemperatures()
|
tempSensors, err := sensors.SensorsTemperatures()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to retrieve list of sensors: %v\n", err)
|
fmt.Printf("Failed to retrieve list of sensors: %v\n", err)
|
||||||
|
if warns, ok := err.(*sensors.Warnings); ok {
|
||||||
|
for _, w := range warns.List {
|
||||||
|
fmt.Printf(" - %v\n", w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,3 +117,23 @@ func cliSensorsPrint() int {
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cliMountpointInfo(requestedPath string) int {
|
||||||
|
usage, err := disk.Usage(requestedPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to retrieve info for path %s: %v\n", requestedPath, err)
|
||||||
|
if warns, ok := err.(*disk.Warnings); ok {
|
||||||
|
for _, w := range warns.List {
|
||||||
|
fmt.Printf(" - %v\n", w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Path:", usage.Path)
|
||||||
|
fmt.Println("FS type:", ternary(usage.Fstype == "", "unknown", usage.Fstype))
|
||||||
|
fmt.Printf("Used percent: %.1f%%\n", usage.UsedPercent)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
|
@ -51,6 +51,8 @@ func Main() int {
|
||||||
fmt.Println(string(contents))
|
fmt.Println(string(contents))
|
||||||
case cliIntentSensorsPrint:
|
case cliIntentSensorsPrint:
|
||||||
return cliSensorsPrint()
|
return cliSensorsPrint()
|
||||||
|
case cliIntentMountpointInfo:
|
||||||
|
return cliMountpointInfo(options.args[1])
|
||||||
case cliIntentDiagnose:
|
case cliIntentDiagnose:
|
||||||
runDiagnostic()
|
runDiagnostic()
|
||||||
}
|
}
|
||||||
|
|
|
@ -649,7 +649,7 @@ function setupTruncatedElementTitles() {
|
||||||
|
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
const element = elements[i];
|
const element = elements[i];
|
||||||
if (element.title === "") element.title = element.textContent;
|
if (element.getAttribute("title") === null) element.title = element.textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2038,6 +2038,7 @@ details[open] .summary::after {
|
||||||
.color-primary { color: var(--color-primary); }
|
.color-primary { color: var(--color-primary); }
|
||||||
|
|
||||||
.cursor-help { cursor: help; }
|
.cursor-help { cursor: help; }
|
||||||
|
.rounded { border-radius: var(--border-radius); }
|
||||||
.break-all { word-break: break-all; }
|
.break-all { word-break: break-all; }
|
||||||
.text-left { text-align: left; }
|
.text-left { text-align: left; }
|
||||||
.text-right { text-align: right; }
|
.text-right { text-align: right; }
|
||||||
|
|
|
@ -471,6 +471,13 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
||||||
"replaceAll": func(old, new, s string) string {
|
"replaceAll": func(old, new, s string) string {
|
||||||
return strings.ReplaceAll(s, old, new)
|
return strings.ReplaceAll(s, old, new)
|
||||||
},
|
},
|
||||||
|
"replaceMatches": func(pattern, replacement, s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCachedRegexp(pattern).ReplaceAllString(s, replacement)
|
||||||
|
},
|
||||||
"findMatch": func(pattern, s string) string {
|
"findMatch": func(pattern, s string) string {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return ""
|
return ""
|
||||||
|
@ -536,6 +543,18 @@ var customAPITemplateFuncs = func() template.FuncMap {
|
||||||
"concat": func(items ...string) string {
|
"concat": func(items ...string) string {
|
||||||
return strings.Join(items, "")
|
return strings.Join(items, "")
|
||||||
},
|
},
|
||||||
|
"unique": func(key string, results []decoratedGJSONResult) []decoratedGJSONResult {
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
out := make([]decoratedGJSONResult, 0, len(results))
|
||||||
|
for _, result := range results {
|
||||||
|
val := result.String(key)
|
||||||
|
if _, ok := seen[val]; !ok {
|
||||||
|
seen[val] = struct{}{}
|
||||||
|
out = append(out, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range globalTemplateFunctions {
|
for key, value := range globalTemplateFunctions {
|
||||||
|
|
|
@ -331,6 +331,7 @@ func fetchItemsFromRSSFeeds(requests []rssFeedRequest) (rssFeedItemList, error)
|
||||||
|
|
||||||
failed := 0
|
failed := 0
|
||||||
entries := make(rssFeedItemList, 0, len(feeds)*10)
|
entries := make(rssFeedItemList, 0, len(feeds)*10)
|
||||||
|
seen := make(map[string]struct{})
|
||||||
|
|
||||||
for i := range feeds {
|
for i := range feeds {
|
||||||
if errs[i] != nil {
|
if errs[i] != nil {
|
||||||
|
@ -339,7 +340,13 @@ func fetchItemsFromRSSFeeds(requests []rssFeedRequest) (rssFeedItemList, error)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entries = append(entries, feeds[i]...)
|
for _, item := range feeds[i] {
|
||||||
|
if _, exists := seen[item.Link]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, item)
|
||||||
|
seen[item.Link] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if failed == len(requests) {
|
if failed == len(requests) {
|
||||||
|
|
|
@ -196,6 +196,10 @@ func fetchChannelFromTwitchTask(channel string) (twitchChannel, error) {
|
||||||
slog.Warn("Failed to parse Twitch stream started at", "error", err, "started_at", streamMetadata.UserOrNull.Stream.StartedAt)
|
slog.Warn("Failed to parse Twitch stream started at", "error", err, "started_at", streamMetadata.UserOrNull.Stream.StartedAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// This prevents live channels with 0 viewers from being
|
||||||
|
// incorrectly sorted lower than offline channels
|
||||||
|
result.ViewersCount = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|
|
@ -181,6 +181,11 @@ func workerPoolDo[I any, O any](job *workerPoolJob[I, O]) ([]O, []error, error)
|
||||||
return results, errs, nil
|
return results, errs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(job.data) == 1 {
|
||||||
|
results[0], errs[0] = job.task(job.data[0])
|
||||||
|
return results, errs, nil
|
||||||
|
}
|
||||||
|
|
||||||
tasksQueue := make(chan *workerPoolTask[I, O])
|
tasksQueue := make(chan *workerPoolTask[I, O])
|
||||||
resultsQueue := make(chan *workerPoolTask[I, O])
|
resultsQueue := make(chan *workerPoolTask[I, O])
|
||||||
|
|
||||||
|
|
|
@ -227,35 +227,50 @@ func Collect(req *SystemInfoRequest) (*SystemInfo, []error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filesystems, err := disk.Partitions(false)
|
addedMountpoints := map[string]struct{}{}
|
||||||
if err == nil {
|
addMountpointInfo := func(requestedPath string, mpReq MointpointRequest) {
|
||||||
for _, fs := range filesystems {
|
if _, exists := addedMountpoints[requestedPath]; exists {
|
||||||
mpReq, ok := req.Mountpoints[fs.Mountpoint]
|
return
|
||||||
isHidden := req.HideMountpointsByDefault
|
|
||||||
if ok && mpReq.Hide != nil {
|
|
||||||
isHidden = *mpReq.Hide
|
|
||||||
}
|
|
||||||
if isHidden {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
usage, err := disk.Usage(fs.Mountpoint)
|
|
||||||
if err == nil {
|
|
||||||
mpInfo := MountpointInfo{
|
|
||||||
Path: fs.Mountpoint,
|
|
||||||
Name: mpReq.Name,
|
|
||||||
TotalMB: usage.Total / 1024 / 1024,
|
|
||||||
UsedMB: usage.Used / 1024 / 1024,
|
|
||||||
UsedPercent: uint8(math.Min(usage.UsedPercent, 100)),
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Mountpoints = append(info.Mountpoints, mpInfo)
|
|
||||||
} else {
|
|
||||||
addErr(fmt.Errorf("getting filesystem usage for %s: %v", fs.Mountpoint, err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
addErr(fmt.Errorf("getting filesystems: %v", err))
|
isHidden := req.HideMountpointsByDefault
|
||||||
|
if mpReq.Hide != nil {
|
||||||
|
isHidden = *mpReq.Hide
|
||||||
|
}
|
||||||
|
if isHidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
usage, err := disk.Usage(requestedPath)
|
||||||
|
if err == nil {
|
||||||
|
mpInfo := MountpointInfo{
|
||||||
|
Path: requestedPath,
|
||||||
|
Name: mpReq.Name,
|
||||||
|
TotalMB: usage.Total / 1024 / 1024,
|
||||||
|
UsedMB: usage.Used / 1024 / 1024,
|
||||||
|
UsedPercent: uint8(math.Min(usage.UsedPercent, 100)),
|
||||||
|
}
|
||||||
|
|
||||||
|
info.Mountpoints = append(info.Mountpoints, mpInfo)
|
||||||
|
addedMountpoints[requestedPath] = struct{}{}
|
||||||
|
} else {
|
||||||
|
addErr(fmt.Errorf("getting filesystem usage for %s: %v", requestedPath, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !req.HideMountpointsByDefault {
|
||||||
|
filesystems, err := disk.Partitions(false)
|
||||||
|
if err == nil {
|
||||||
|
for _, fs := range filesystems {
|
||||||
|
addMountpointInfo(fs.Mountpoint, req.Mountpoints[fs.Mountpoint])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addErr(fmt.Errorf("getting filesystems: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for mountpoint, mpReq := range req.Mountpoints {
|
||||||
|
addMountpointInfo(mountpoint, mpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(info.Mountpoints, func(a, b int) bool {
|
sort.Slice(info.Mountpoints, func(a, b int) bool {
|
||||||
|
|
Loading…
Add table
Reference in a new issue