Nowosci i poprawki:

- Nowy widget do beszel wraz z dokumentacja
- Zmiana wielkosci widgetu radyjko
pull/878/head
jandziaslo 2025-11-19 20:39:06 +07:00
parent 2e8eb157ee
commit 6009e0579e
No known key found for this signature in database
GPG Key ID: E939F8F12F8D3A5A
5 changed files with 264 additions and 17 deletions

@ -0,0 +1,76 @@
# Widget Beszel - Instrukcja użycia
Widget Beszel pozwala na monitorowanie statusu serwerów w czasie rzeczywistym, wykorzystując lekkie i nowoczesne narzędzie monitoringu Beszel.
## Konfiguracja
Aby skonfigurować widget Beszel, dodaj następującą konfigurację do swojego pliku `glance.yml`:
```yaml
- type: beszel
url: https://twoja-instancja-beszel.pl # URL do Twojej instancji Beszel (API)
redirect-url: https://twoja-instancja-beszel.pl # URL do interfejsu webowego Beszel
token: twoj-token-jwt # Token JWT (
cache: 10s # Częstotliwość odświeżania (domyślnie 10s)
```
### Uzyskiwanie tokenu
Jeśli Twoja instancja Beszel jest zabezpieczona i nie udostępnia danych publicznie, będziesz potrzebować tokenu.
Obecnie widget obsługuje tokeny Bearer. Możesz uzyskać token logując się do Beszel i sprawdzając żądania sieciowe w przeglądarce lub generując go w panelu administracyjnym (jeśli dostępne).
## Funkcje widgetu
### 1. Monitorowanie statusu serwerów
Widget wyświetla listę serwerów z następującymi informacjami:
- **Nazwa serwera**: Nazwa zdefiniowana w Beszel.
- **Status**: Ikona serwera zmienia kolor w zależności od dostępności (zielony - online, czerwony - offline).
- **Uptime**: Czas nieprzerwanej pracy serwera (np. "5 days uptime").
### 2. Szczegółowe metryki
Dla każdego serwera wyświetlane są paski postępu z aktualnym użyciem zasobów:
- **CPU**: Aktualne użycie procesora w procentach.
- **RAM**: Aktualne użycie pamięci RAM w procentach.
- **DISK**: Zajętość głównego dysku w procentach.
### 3. Dodatkowe informacje (Popover)
Po najechaniu kursorem na ikonę serwera lub paski postępu, wyświetlane są dodatkowe informacje w dymku:
- **Host/IP**: Adres IP lub nazwa hosta serwera.
- **Kernel**: Wersja jądra systemu.
- **CPU Model**: Model procesora.
- **Load Average**: Średnie obciążenie systemu (1m, 5m, 15m) - dostępne po najechaniu na pasek CPU.
### 4. Linkowanie do systemu
Jeśli skonfigurowano parametr `redirect-url`, kliknięcie w nazwę serwera otworzy nową kartę z szczegółowymi statystykami tego konkretnego systemu w panelu Beszel.
## Przykładowa konfiguracja
```yaml
pages:
- name: Home
columns:
- size: small
widgets:
- type: beszel
title: Serwery
url: https://beszel.example.com
redirect-url: https://beszel.example.com
token: TWóJ_TOKEN
cache: 5s
```
## Rozwiązywanie problemów
### Widget nie wyświetla danych
1. Sprawdź czy URL do instancji Beszel jest poprawny i dostępny z serwera, na którym działa Glance.
2. Upewnij się, że endpoint `/api/collections/systems/records` jest dostępny.
3. Jeśli Twoja instancja wymaga autoryzacji, upewnij się, że podałeś poprawny token.
### Brakujące metryki
Niektóre metryki (np. Load Average) mogą nie być dostępne w zależności od wersji agenta Beszel zainstalowanego na monitorowanym serwerze.

@ -7,7 +7,7 @@
.radyjko-player {
position: relative;
height: 100%;
min-height: 380px;
min-height: 280px;
max-height: 100%;
display: flex;
flex-direction: column;
@ -50,13 +50,13 @@
display: flex;
align-items: center;
justify-content: center;
padding: 24px 20px 16px;
padding: 16px 16px 8px;
flex-shrink: 0;
}
.radyjko-album-art {
width: 140px;
height: 140px;
width: 100px;
height: 100px;
border-radius: 12px;
background: var(--color-widget-background-highlight);
display: flex;
@ -85,7 +85,7 @@
position: relative;
z-index: 2;
text-align: center;
padding: 16px 16px 8px;
padding: 8px 16px 4px;
width: 100%;
min-width: 0;
}
@ -111,11 +111,11 @@
.radyjko-controls {
position: relative;
z-index: 2;
padding: 16px 16px;
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
gap: 16px;
width: 100%;
flex-shrink: 0;
}
@ -126,14 +126,14 @@
color: var(--color-text-highlight);
border: 1px solid var(--color-widget-content-border);
border-radius: 50%;
width: 48px;
height: 48px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1.6rem;
font-size: 1.4rem;
box-shadow: 0 2px 8px hsla(var(--bghs), var(--bgl), 0.1);
font-weight: 500;
}
@ -154,17 +154,17 @@
}
.radyjko-btn.radyjko-btn-play-main {
width: 56px;
height: 56px;
width: 48px;
height: 48px;
background: var(--color-primary);
color: hsl(var(--bghs), var(--bgl));
border-color: var(--color-primary);
font-size: 2rem;
font-size: 1.8rem;
}
.radyjko-btn.radyjko-btn-play-main svg {
width: 28px;
height: 28px;
width: 24px;
height: 24px;
}
.radyjko-btn.radyjko-btn-prev,
@ -172,8 +172,8 @@
background: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 4%)));
color: var(--color-text-highlight);
border: 1px solid hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 8%)));
width: 44px;
height: 44px;
width: 36px;
height: 36px;
}
.radyjko-btn.radyjko-btn-prev:hover,

@ -0,0 +1,88 @@
{{ template "widget-base.html" . }}
{{- define "widget-content" }}
{{- $redirect := .RedirectURL }}
{{- range .Systems }}
<div class="server">
<div class="server-info">
<div class="server-details">
<div class="server-name size-h3">
{{ if ne $redirect "" }}
<a class="color-highlight" href="{{ $redirect }}/system/{{ .Name }}" target="_blank" rel="noopener">{{ .Name }}</a>
{{ else }}
<span class="color-highlight">{{ .Name }}</span>
{{ end }}
</div>
<div>
{{ if eq .Status "up" }}
<span {{ dynamicRelativeTimeAttrs .BootTime }}></span> uptime
{{ else }}
<span class="color-negative">down</span>
{{ end }}
</div>
</div>
<div class="shrink-0" data-popover-type="html" data-popover-margin="0.2rem" data-popover-max-width="400px">
<div data-popover-html>
<div class="size-h5 text-compact">HOST</div>
<div class="color-highlight">{{ .Host }}</div>
<div class="size-h5 text-compact margin-top-3">KERNEL</div>
<div class="color-highlight">{{ .Info.Kernel }}</div>
<div class="size-h5 text-compact margin-top-3">CPU MODEL</div>
<div class="color-highlight">{{ .Info.CPUModel }}</div>
</div>
<svg class="server-icon" stroke="var(--color-{{ if eq .Status "up" }}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">
<div class="flex items-end size-h5">
<div>CPU</div>
<div class="color-highlight margin-left-auto text-very-compact">{{ printf "%.1f" .Info.CPU }} <span class="color-base">%</span></div>
</div>
<div class="progress-bar" data-popover-type="html">
<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.Load1 }}</div>
</div>
<div class="flex margin-top-3">
<div class="size-h5">5M AVG</div>
<div class="value-separator"></div>
<div class="color-highlight text-very-compact">{{ .Info.Load5 }}</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.Load15 }}</div>
</div>
</div>
<div class="progress-value{{ if ge .Info.CPU 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.CPU }}"></div>
</div>
</div>
<div class="flex-1">
<div class="flex items-end size-h5">
<div>RAM</div>
<div class="color-highlight margin-left-auto text-very-compact">{{ printf "%.1f" .Info.Memory }} <span class="color-base">%</span></div>
</div>
<div class="progress-bar">
<div class="progress-value{{ if ge .Info.Memory 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.Memory }}"></div>
</div>
</div>
<div class="flex-1">
<div class="flex items-end size-h5">
<div>DISK</div>
<div class="color-highlight margin-left-auto text-very-compact">{{ printf "%.1f" .Info.Disk }} <span class="color-base">%</span></div>
</div>
<div class="progress-bar">
<div class="progress-value{{ if ge .Info.Disk 85.0 }} progress-value-notice{{ end }}" style="--percent: {{ .Info.Disk }}"></div>
</div>
</div>
</div>
</div>
{{- end }}
{{- end }}

@ -0,0 +1,81 @@
package glance
import (
"context"
"errors"
"html/template"
"net/http"
"time"
)
var beszelWidgetTemplate = mustParseTemplate("beszel.html", "widget-base.html")
type beszelWidget struct {
widgetBase `yaml:",inline"`
URL string `yaml:"url"`
Token string `yaml:"token"`
RedirectURL string `yaml:"redirect-url"`
Systems []beszelSystem `yaml:"-"`
}
type beszelResponse struct {
Items []beszelSystem `json:"items"`
}
type beszelSystem struct {
Name string `json:"name"`
Host string `json:"host"`
Status string `json:"status"`
Info beszelInfo `json:"info"`
BootTime time.Time `json:"-"`
}
type beszelInfo struct {
Kernel string `json:"k"`
Uptime float64 `json:"u"`
CPUModel string `json:"m"`
CPU float64 `json:"cpu"`
Memory float64 `json:"mp"`
Disk float64 `json:"dp"`
Load1 float64 `json:"l1"`
Load5 float64 `json:"l5"`
Load15 float64 `json:"l15"`
}
func (w *beszelWidget) initialize() error {
w.withTitle("Beszel").withCacheDuration(10 * time.Second)
if w.URL == "" {
return errors.New("beszel widget: url is required")
}
return nil
}
func (w *beszelWidget) update(ctx context.Context) {
req, err := http.NewRequestWithContext(ctx, "GET", w.URL+"/api/collections/systems/records", nil)
if err != nil {
w.withError(err)
return
}
if w.Token != "" {
req.Header.Set("Authorization", "Bearer "+w.Token)
}
resp, err := decodeJsonFromRequest[*beszelResponse](defaultHTTPClient, req)
if err != nil {
w.withError(err)
return
}
w.Systems = resp.Items
now := time.Now()
for i := range w.Systems {
w.Systems[i].BootTime = now.Add(-time.Duration(w.Systems[i].Info.Uptime) * time.Second)
}
w.withError(nil).scheduleNextUpdate()
}
func (w *beszelWidget) Render() template.HTML {
return w.renderTemplate(w, beszelWidgetTemplate)
}

@ -87,6 +87,8 @@ func newWidget(widgetType string) (widget, error) {
w = &vikunjaWidget{}
case "tailscale":
w = &tailscaleWidget{}
case "beszel":
w = &beszelWidget{}
default:
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
}