mirror of
https://github.com/Xevion/glance.git
synced 2025-12-10 22:07:21 -06:00
Add server-stats widget
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
--color-popover-background: hsl(var(--bgh), calc(var(--bgs) + 3%), calc(var(--bgl) + 3%));
|
||||
--color-popover-border: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 12%)));
|
||||
--color-progress-border: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 10% * var(--cm))));
|
||||
--color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 27% * var(--cm))));
|
||||
--color-progress-value: hsl(var(--bgh), calc(var(--bgs) * var(--tsm)), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 30% * var(--cm))));
|
||||
--color-graph-gridlines: hsl(var(--bghs), calc(var(--scheme) ((var(--scheme) var(--bgl)) + 6% * var(--cm))));
|
||||
|
||||
--ths: var(--bgh), calc(var(--bgs) * var(--tsm));
|
||||
@@ -796,6 +796,20 @@ details[open] .summary::after {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.widget-beta-icon {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
flex-shrink: 0;
|
||||
transition: transform .45s, opacity .45s, stroke .45s;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.widget-beta-icon:hover, .widget-header .popover-active > .widget-beta-icon {
|
||||
fill: var(--color-text-highlight);
|
||||
transform: translateY(-10%) scale(1.3);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.widget + .widget {
|
||||
margin-top: var(--widget-gap);
|
||||
}
|
||||
@@ -1484,6 +1498,137 @@ details[open] .summary::after {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.widget-type-server-info {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.server + .server {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.server {
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-details {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-icon {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.server-spicy-cpu-icon {
|
||||
height: 1em;
|
||||
align-self: center;
|
||||
margin-left: 0.4em;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.server-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.server-stat-unavailable {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border: 1px solid var(--color-progress-border);
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
height: 1.5rem;
|
||||
margin-inline: -3px; /* naughty, but oh so beautiful */
|
||||
}
|
||||
|
||||
.progress-bar-combined {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.popover-active > .progress-bar {
|
||||
transition: border-color .3s;
|
||||
border-color: var(--color-text-subdue);
|
||||
}
|
||||
|
||||
.progress-value {
|
||||
--half-border-radius: calc(var(--border-radius) / 2);
|
||||
border-radius: 0 var(--half-border-radius) var(--half-border-radius) 0;
|
||||
background: var(--color-progress-value);
|
||||
width: calc(var(--percent) * 1%);
|
||||
min-width: 1px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-value:first-child {
|
||||
border-top-left-radius: var(--half-border-radius);
|
||||
}
|
||||
|
||||
.progress-value:last-child {
|
||||
border-bottom-left-radius: var(--half-border-radius);
|
||||
}
|
||||
|
||||
.progress-value-notice {
|
||||
background: linear-gradient(to right, var(--color-progress-value) 65%, var(--color-negative));
|
||||
}
|
||||
|
||||
.value-separator {
|
||||
min-width: 2rem;
|
||||
margin-inline: 0.8rem;
|
||||
flex: 1;
|
||||
height: calc(1em * 1.1);
|
||||
border-bottom: 1px dotted var(--color-text-subdue);
|
||||
}
|
||||
|
||||
@container widget (min-width: 650px) {
|
||||
.server {
|
||||
gap: 2rem;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.server + .server {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.server-info {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: unset;
|
||||
margin-right: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.server-stats {
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
min-width: 450px;
|
||||
margin-top: 0;
|
||||
gap: 2rem;
|
||||
padding-bottom: 0.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.server-stats > * {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
filter: grayscale(0.2) contrast(0.9);
|
||||
opacity: 0.8;
|
||||
@@ -1881,6 +2026,7 @@ details[open] .summary::after {
|
||||
.text-center { text-align: center; }
|
||||
.text-elevate { margin-top: -0.2em; }
|
||||
.text-compact { word-spacing: -0.18em; }
|
||||
.text-very-compact { word-spacing: -0.35em; }
|
||||
.rtl { direction: rtl; }
|
||||
.shrink { flex-shrink: 1; }
|
||||
.shrink-0 { flex-shrink: 0; }
|
||||
@@ -1891,6 +2037,7 @@ details[open] .summary::after {
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.relative { position: relative; }
|
||||
.flex { display: flex; }
|
||||
.flex-1 { flex: 1; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
@@ -1903,6 +2050,7 @@ details[open] .summary::after {
|
||||
.flex-column { flex-direction: column; }
|
||||
.items-center { align-items: center; }
|
||||
.items-start { align-items: start; }
|
||||
.items-end { align-items: end; }
|
||||
.gap-5 { gap: 0.5rem; }
|
||||
.gap-7 { gap: 0.7rem; }
|
||||
.gap-10 { gap: 1rem; }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package glance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"golang.org/x/text/message"
|
||||
@@ -27,9 +27,31 @@ var globalTemplateFunctions = template.FuncMap{
|
||||
"formatPrice": func(price float64) string {
|
||||
return intl.Sprintf("%.2f", price)
|
||||
},
|
||||
"dynamicRelativeTimeAttrs": func(t time.Time) template.HTMLAttr {
|
||||
"dynamicRelativeTimeAttrs": func(t interface{ Unix() int64 }) template.HTMLAttr {
|
||||
return template.HTMLAttr(`data-dynamic-relative-time="` + strconv.FormatInt(t.Unix(), 10) + `"`)
|
||||
},
|
||||
"formatServerMegabytes": func(mb uint64) template.HTML {
|
||||
var value string
|
||||
var label string
|
||||
|
||||
if mb < 1_000 {
|
||||
value = strconv.FormatUint(mb, 10)
|
||||
label = "MB"
|
||||
} else if mb < 1_000_000 {
|
||||
if mb < 10_000 {
|
||||
value = fmt.Sprintf("%.1f", float64(mb)/1_000)
|
||||
} else {
|
||||
value = strconv.FormatUint(mb/1_000, 10)
|
||||
}
|
||||
|
||||
label = "GB"
|
||||
} else {
|
||||
value = fmt.Sprintf("%.1f", float64(mb)/1_000_000)
|
||||
label = "TB"
|
||||
}
|
||||
|
||||
return template.HTML(value + ` <span class="color-base size-h5">` + label + `</span>`)
|
||||
},
|
||||
}
|
||||
|
||||
func mustParseTemplate(primary string, dependencies ...string) *template.Template {
|
||||
|
||||
140
internal/glance/templates/server-stats.html
Normal file
140
internal/glance/templates/server-stats.html
Normal file
@@ -0,0 +1,140 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{- define "widget-content" }}
|
||||
{{- range .Servers }}
|
||||
<div class="server">
|
||||
<div class="server-info">
|
||||
<div class="server-details">
|
||||
<div class="server-name color-highlight size-h3">{{ if .Name }}{{ .Name }}{{ else }}{{ .Info.Hostname }}{{ end }}</div>
|
||||
<div>
|
||||
{{- if .IsReachable }}
|
||||
{{ if .Info.HostInfoIsAvailable }}<span {{ dynamicRelativeTimeAttrs .Info.BootTime }}></span>{{ else }}unknown{{ end }} uptime
|
||||
{{- else }}
|
||||
unreachable
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shrink-0"{{ if .IsReachable }} data-popover-type="html" data-popover-margin="0.2rem" data-popover-max-width="400px"{{ end }}>
|
||||
{{- if .IsReachable }}
|
||||
<div data-popover-html>
|
||||
<div class="size-h5 text-compact">PLATFORM</div>
|
||||
<div class="color-highlight">{{ if .Info.HostInfoIsAvailable }}{{ .Info.Platform }}{{ else }}Unknown{{ end }}</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
<svg class="server-icon" stroke="var(--color-{{ if .IsReachable }}positive{{ else }}negative{{ end }})" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 17.25v-.228a4.5 4.5 0 0 0-.12-1.03l-2.268-9.64a3.375 3.375 0 0 0-3.285-2.602H7.923a3.375 3.375 0 0 0-3.285 2.602l-2.268 9.64a4.5 4.5 0 0 0-.12 1.03v.228m19.5 0a3 3 0 0 1-3 3H5.25a3 3 0 0 1-3-3m19.5 0a3 3 0 0 0-3-3H5.25a3 3 0 0 0-3 3m16.5 0h.008v.008h-.008v-.008Zm-3 0h.008v.008h-.008v-.008Z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-stats">
|
||||
<div class="flex-1{{ if not .Info.CPU.LoadIsAvailable }} server-stat-unavailable{{ end }}">
|
||||
<div class="flex items-end size-h5">
|
||||
<div>CPU</div>
|
||||
{{- if and .Info.CPU.TemperatureIsAvailable (ge .Info.CPU.TemperatureC 80) }}
|
||||
<svg class="server-spicy-cpu-icon" fill="var(--color-negative)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" >
|
||||
<path fill-rule="evenodd" d="M8.074.945A4.993 4.993 0 0 0 6 5v.032c.004.6.114 1.176.311 1.709.16.428-.204.91-.61.7a5.023 5.023 0 0 1-1.868-1.677c-.202-.304-.648-.363-.848-.058a6 6 0 1 0 8.017-1.901l-.004-.007a4.98 4.98 0 0 1-2.18-2.574c-.116-.31-.477-.472-.744-.28Zm.78 6.178a3.001 3.001 0 1 1-3.473 4.341c-.205-.365.215-.694.62-.59a4.008 4.008 0 0 0 1.873.03c.288-.065.413-.386.321-.666A3.997 3.997 0 0 1 8 8.999c0-.585.126-1.14.351-1.641a.42.42 0 0 1 .503-.235Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{{- end }}
|
||||
<div class="color-highlight margin-left-auto text-very-compact">{{ if .Info.CPU.LoadIsAvailable }}{{ .Info.CPU.Load1Percent }} <span class="color-base">%</span>{{ else }}n/a{{ end }}</div>
|
||||
</div>
|
||||
<div{{ if .Info.CPU.LoadIsAvailable }} data-popover-type="html"{{ end }}>
|
||||
{{- if .Info.CPU.LoadIsAvailable }}
|
||||
<div data-popover-html>
|
||||
<div class="flex">
|
||||
<div class="size-h5">1M AVG</div>
|
||||
<div class="value-separator"></div>
|
||||
<div class="color-highlight text-very-compact">{{ .Info.CPU.Load1Percent }} <span class="color-base size-h5">%</span></div>
|
||||
</div>
|
||||
<div class="flex margin-top-3">
|
||||
<div class="size-h5">15M AVG</div>
|
||||
<div class="value-separator"></div>
|
||||
<div class="color-highlight text-very-compact">{{ .Info.CPU.Load15Percent }} <span class="color-base size-h5">%</span></div>
|
||||
</div>
|
||||
{{- if .Info.CPU.TemperatureIsAvailable }}
|
||||
<div class="flex margin-top-3">
|
||||
<div class="size-h5">TEMP C</div>
|
||||
<div class="value-separator"></div>
|
||||
<div class="color-highlight text-very-compact">{{ .Info.CPU.TemperatureC }} <span class="color-base size-h5">°</span></div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
<div class="progress-bar progress-bar-combined">
|
||||
{{- if .Info.CPU.LoadIsAvailable }}
|
||||
<div class="progress-value{{ if ge .Info.CPU.Load1Percent 85 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.CPU.Load1Percent }}"></div>
|
||||
<div class="progress-value{{ if ge .Info.CPU.Load15Percent 85 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.CPU.Load15Percent }}"></div>
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1{{ if not .Info.Memory.IsAvailable }} server-stat-unavailable{{ end }}">
|
||||
<div class="flex justify-between items-end size-h5">
|
||||
<div>RAM</div>
|
||||
<div class="color-highlight text-very-compact">{{ if .Info.Memory.IsAvailable }}{{ .Info.Memory.UsedPercent }} <span class="color-base">%</span>{{ else }}n/a{{ end }}</div>
|
||||
</div>
|
||||
<div{{ if .Info.Memory.IsAvailable }} data-popover-type="html"{{ end }}>
|
||||
{{- if .Info.Memory.IsAvailable }}
|
||||
<div data-popover-html>
|
||||
<div class="flex">
|
||||
<div class="size-h5">RAM</div>
|
||||
<div class="value-separator"></div>
|
||||
<div class="color-highlight text-very-compact">
|
||||
{{ .Info.Memory.UsedMB | formatServerMegabytes }} <span class="color-base size-h5">/</span> {{ .Info.Memory.TotalMB | formatServerMegabytes }}
|
||||
</div>
|
||||
</div>
|
||||
{{- if and (not .HideSwap) .Info.Memory.SwapIsAvailable }}
|
||||
<div class="flex margin-top-3">
|
||||
<div class="size-h5">SWAP</div>
|
||||
<div class="value-separator"></div>
|
||||
<div class="color-highlight text-very-compact">
|
||||
{{ .Info.Memory.SwapUsedMB | formatServerMegabytes }} <span class="color-base size-h5">/</span> {{ .Info.Memory.SwapTotalMB | formatServerMegabytes }}
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
</div>
|
||||
{{- end }}
|
||||
<div class="progress-bar progress-bar-combined">
|
||||
{{- if .Info.Memory.IsAvailable }}
|
||||
<div class="progress-value{{ if ge .Info.Memory.UsedPercent 85 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.Memory.UsedPercent }}"></div>
|
||||
{{- if and (not .HideSwap) .Info.Memory.SwapIsAvailable }}
|
||||
<div class="progress-value{{ if ge .Info.Memory.SwapUsedPercent 85 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.Memory.SwapUsedPercent }}"></div>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1{{ if not .Info.Mountpoints }} server-stat-unavailable{{ end }}">
|
||||
<div class="flex justify-between items-end size-h5">
|
||||
<div>DISK</div>
|
||||
<div class="color-highlight text-very-compact">{{ if .Info.Mountpoints }}{{ (index .Info.Mountpoints 0).UsedPercent }} <span class="color-base">%</span>{{ else }}n/a{{ end }}</div>
|
||||
</div>
|
||||
<div{{ if .Info.Mountpoints }} data-popover-type="html"{{ end }}>
|
||||
{{- if .Info.Mountpoints }}
|
||||
<div data-popover-html>
|
||||
<ul class="list list-gap-2">
|
||||
{{- range .Info.Mountpoints }}
|
||||
<li class="flex">
|
||||
<div class="size-h5">{{ if .Name }}{{ .Name }}{{ else }}{{ .Path }}{{ end }}</div>
|
||||
<div class="value-separator"></div>
|
||||
<div class="color-highlight text-very-compact">
|
||||
{{ .UsedMB | formatServerMegabytes }} <span class="color-base size-h5">/</span> {{ .TotalMB | formatServerMegabytes }}
|
||||
</div>
|
||||
</li>
|
||||
{{- end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{- end }}
|
||||
<div class="progress-bar progress-bar-combined">
|
||||
{{- if .Info.Mountpoints }}
|
||||
<div class="progress-value{{ if ge ((index .Info.Mountpoints 0).UsedPercent) 85 }} progress-value-notice{{ end }}" style="--percent: {{ (index .Info.Mountpoints 0).UsedPercent }}"></div>
|
||||
{{- if ge (len .Info.Mountpoints) 2 }}
|
||||
<div class="progress-value{{ if ge ((index .Info.Mountpoints 1).UsedPercent) 85 }} progress-value-notice{{ end }}" style="--percent: {{ (index .Info.Mountpoints 1).UsedPercent }}"></div>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -1,18 +1,34 @@
|
||||
<div class="widget widget-type-{{ .GetType }}{{ if ne "" .CSSClass }} {{ .CSSClass }}{{ end }}">
|
||||
{{ if not .HideHeader}}
|
||||
{{- if not .HideHeader}}
|
||||
<div class="widget-header">
|
||||
{{ if ne "" .TitleURL }}<a href="{{ .TitleURL | safeURL }}" target="_blank" rel="noreferrer" class="uppercase">{{ .Title }}</a>{{ else }}<div class="uppercase">{{ .Title }}</div>{{ end }}
|
||||
{{ if and .Error .ContentAvailable }}
|
||||
{{- if ne "" .TitleURL }}
|
||||
<a href="{{ .TitleURL | safeURL }}" target="_blank" rel="noreferrer" class="uppercase">{{ .Title }}</a>
|
||||
{{- else }}
|
||||
<div class="uppercase">{{ .Title }}</div>
|
||||
{{- end }}
|
||||
{{- if .IsWIP }}
|
||||
<div data-popover-type="html" data-popover-position="above">
|
||||
<div data-popover-html>
|
||||
<p class="size-h5">WORK IN PROGRESS</p>
|
||||
<p class="margin-block-10 color-paragraph">This widget is still in development, certain features may not work as expected or may change drastically.</p>
|
||||
<a class="color-primary visited-indicator" href="https://github.com/glanceapp/glance/issues" target="_blank" rel="noreferrer">Report issue</a>
|
||||
</div>
|
||||
<svg class="widget-beta-icon cursor-help" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M19 5.5a4.5 4.5 0 0 1-4.791 4.49c-.873-.055-1.808.128-2.368.8l-6.024 7.23a2.724 2.724 0 1 1-3.837-3.837L9.21 8.16c.672-.56.855-1.495.8-2.368a4.5 4.5 0 0 1 5.873-4.575c.324.105.39.51.15.752L13.34 4.66a.455.455 0 0 0-.11.494 3.01 3.01 0 0 0 1.617 1.617c.17.07.363.02.493-.111l2.692-2.692c.241-.241.647-.174.752.15.14.435.216.9.216 1.382ZM4 17a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{- end }}
|
||||
{{- if and .Error .ContentAvailable }}
|
||||
<div class="notice-icon notice-icon-major" title="{{ .Error }}"></div>
|
||||
{{ else if .Notice }}
|
||||
{{- else if .Notice }}
|
||||
<div class="notice-icon notice-icon-minor" title="{{ .Notice }}"></div>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
<div class="widget-content{{ if .ContentAvailable }} {{ block "widget-content-classes" . }}{{ end }}{{ end }}">
|
||||
{{ if .ContentAvailable }}
|
||||
{{ block "widget-content" . }}{{ end }}
|
||||
{{ else }}
|
||||
{{- if .ContentAvailable }}
|
||||
{{ block "widget-content" . }}{{ end }}
|
||||
{{- else }}
|
||||
<div class="widget-error-header">
|
||||
<div class="color-negative size-h3">ERROR</div>
|
||||
<svg class="widget-error-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
@@ -20,6 +36,6 @@
|
||||
</svg>
|
||||
</div>
|
||||
<p class="break-all">{{ if .Error }}{{ .Error }}{{ else }}No error information provided{{ end }}</p>
|
||||
{{ end}}
|
||||
{{- end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
117
internal/glance/widget-server-stats.go
Normal file
117
internal/glance/widget-server-stats.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package glance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/pkg/sysinfo"
|
||||
)
|
||||
|
||||
var serverStatsWidgetTemplate = mustParseTemplate("server-stats.html", "widget-base.html")
|
||||
|
||||
type serverStatsWidget struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Servers []serverStatsRequest `yaml:"servers"`
|
||||
}
|
||||
|
||||
func (widget *serverStatsWidget) initialize() error {
|
||||
widget.withTitle("Server Stats").withCacheDuration(15 * time.Second)
|
||||
widget.widgetBase.WIP = true
|
||||
|
||||
if len(widget.Servers) == 0 {
|
||||
widget.Servers = []serverStatsRequest{{Type: "local"}}
|
||||
}
|
||||
|
||||
for i := range widget.Servers {
|
||||
widget.Servers[i].URL = strings.TrimRight(widget.Servers[i].URL, "/")
|
||||
|
||||
if widget.Servers[i].Timeout == 0 {
|
||||
widget.Servers[i].Timeout = durationField(3 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *serverStatsWidget) update(context.Context) {
|
||||
// Refactor later, most of it may change depending on feedback
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := range widget.Servers {
|
||||
serv := &widget.Servers[i]
|
||||
|
||||
if serv.Type == "local" {
|
||||
info, errs := sysinfo.Collect(serv.SystemInfoRequest)
|
||||
|
||||
if len(errs) > 0 {
|
||||
for i := range errs {
|
||||
slog.Warn("Getting system info: " + errs[i].Error())
|
||||
}
|
||||
}
|
||||
|
||||
serv.IsReachable = true
|
||||
serv.Info = info
|
||||
} else {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
info, err := fetchRemoteServerInfo(serv)
|
||||
if err != nil {
|
||||
slog.Warn("Getting remote system info: " + err.Error())
|
||||
serv.IsReachable = false
|
||||
serv.Info = &sysinfo.SystemInfo{
|
||||
Hostname: "Unnamed server #" + strconv.Itoa(i+1),
|
||||
}
|
||||
} else {
|
||||
serv.IsReachable = true
|
||||
serv.Info = info
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
widget.withError(nil).scheduleNextUpdate()
|
||||
}
|
||||
|
||||
func (widget *serverStatsWidget) Render() template.HTML {
|
||||
return widget.renderTemplate(widget, serverStatsWidgetTemplate)
|
||||
}
|
||||
|
||||
type serverStatsRequest struct {
|
||||
*sysinfo.SystemInfoRequest `yaml:",inline"`
|
||||
Info *sysinfo.SystemInfo `yaml:"-"`
|
||||
IsReachable bool `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
Name string `yaml:"name"`
|
||||
HideSwap bool `yaml:"hide-swap"`
|
||||
Type string `yaml:"type"`
|
||||
URL string `yaml:"url"`
|
||||
Token string `yaml:"token"`
|
||||
Timeout durationField `yaml:"timeout"`
|
||||
// Support for other agents
|
||||
// Provider string `yaml:"provider"`
|
||||
}
|
||||
|
||||
func fetchRemoteServerInfo(infoReq *serverStatsRequest) (*sysinfo.SystemInfo, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(infoReq.Timeout))
|
||||
defer cancel()
|
||||
|
||||
request, _ := http.NewRequestWithContext(ctx, "GET", infoReq.URL+"/api/sysinfo/all", nil)
|
||||
if infoReq.Token != "" {
|
||||
request.Header.Set("Authorization", "Bearer "+infoReq.Token)
|
||||
}
|
||||
|
||||
info, err := decodeJsonFromRequest[*sysinfo.SystemInfo](defaultHTTPClient, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
@@ -73,6 +73,8 @@ func newWidget(widgetType string) (widget, error) {
|
||||
w = &customAPIWidget{}
|
||||
case "docker-containers":
|
||||
w = &dockerContainersWidget{}
|
||||
case "server-stats":
|
||||
w = &serverStatsWidget{}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||
}
|
||||
@@ -147,6 +149,7 @@ type widgetBase struct {
|
||||
CSSClass string `yaml:"css-class"`
|
||||
CustomCacheDuration durationField `yaml:"cache"`
|
||||
ContentAvailable bool `yaml:"-"`
|
||||
WIP bool `yaml:"-"`
|
||||
Error error `yaml:"-"`
|
||||
Notice error `yaml:"-"`
|
||||
templateBuffer bytes.Buffer `yaml:"-"`
|
||||
@@ -173,6 +176,10 @@ func (w *widgetBase) requiresUpdate(now *time.Time) bool {
|
||||
return now.After(w.nextUpdate)
|
||||
}
|
||||
|
||||
func (w *widgetBase) IsWIP() bool {
|
||||
return w.WIP
|
||||
}
|
||||
|
||||
func (w *widgetBase) update(ctx context.Context) {
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user