- name: Statistics columns: - size: small widgets: - type: custom-api title: Jellyfin base-url: https://jellyfin.${GROUND_DOMAIN} options: url: https://jellyfin.${GROUND_DOMAIN} key: ${JELLYFIN_API_KEY} template: | {{ $url := .Options.StringOr "url" "" }} {{ $key := .Options.StringOr "key" "" }} {{- if or (eq $url "") (eq $key "") -}}

Error: The URL or API key was not configured in the widget options.

{{- else -}} {{- $auth := printf "MediaBrowser Token=\"%s\"" $key -}} {{- $countsUrl := printf "%s/Items/Counts" $url -}} {{- $jellyfinData := newRequest $countsUrl | withHeader "Authorization" $auth | getResponse -}} {{- $audiobooksUrl := printf "%s/Items?IncludeItemTypes=AudioBook&Recursive=true&Limit=0&EnableTotalRecordCount=true" $url -}} {{- $audiobooksData := newRequest $audiobooksUrl | withHeader "Authorization" $auth | getResponse -}} {{- if and (eq $jellyfinData.Response.StatusCode 200) (eq $audiobooksData.Response.StatusCode 200) -}}
{{ $jellyfinData.JSON.Int "MovieCount" | formatNumber }}
Movies
{{ $jellyfinData.JSON.Int "SeriesCount" | formatNumber }}
TV Shows
{{ $jellyfinData.JSON.Int "EpisodeCount" | formatNumber }}
Episodes
{{ $jellyfinData.JSON.Int "SongCount" | formatNumber }}
Songs
{{ $jellyfinData.JSON.Int "BookCount" | formatNumber }}
Books
{{ $audiobooksData.JSON.Int "TotalRecordCount" | formatNumber }}
Audiobooks
{{- else -}}

Failed to fetch Jellyfin library counts (Counts: {{ $jellyfinData.Response.StatusCode }}, Audiobooks: {{ $audiobooksData.Response.StatusCode }}).

{{- end -}} {{- end -}} - type: custom-api title: SABnzbd cache: 30s url: https://sabnzbd.${GROUND_DOMAIN}/api?output=json&apikey=${SABNZBD_API_KEY}&mode=queue headers: Authorization: Basic ${AUTHENTIK_BASIC_AUTH} Accept: application/json template: |
SPEED
{{ if eq (.JSON.String "queue.status") "Downloading" }}{{ .JSON.String "queue.speed" }}B/s{{ else }}Paused{{ end }}
TIME LEFT
{{ if eq (.JSON.String "queue.status") "Downloading" }}{{ .JSON.String "queue.timeleft" }}{{ else }}--:--:--{{ end }}
QUEUE
{{ .JSON.Int "queue.noofslots" }} items
SIZE
{{ .JSON.Float "queue.mb" | printf "%.1f" }}MB
- type: custom-api title: qBittorrent cache: 10s options: view: "basic" mode: "upload" subrequests: transfer: url: https://qui.${GROUND_DOMAIN}/proxy/${QUI_API_TOKEN}/api/v2/transfer/info seeding: url: https://qui.${GROUND_DOMAIN}/proxy/${QUI_API_TOKEN}/api/v2/torrents/info parameters: filter: seeding leeching: url: https://qui.${GROUND_DOMAIN}/proxy/${QUI_API_TOKEN}/api/v2/torrents/info parameters: filter: downloading template: | {{ $transfer := .Subrequest "transfer" }} {{ $seeding := .Subrequest "seeding" }} {{ $leeching := .Subrequest "leeching" }} {{ if and (eq $transfer.Response.StatusCode 200) (eq $seeding.Response.StatusCode 200) (eq $leeching.Response.StatusCode 200) }} {{ $isDetailed := eq (.Options.StringOr "view" "detailed") "detailed" }} {{ $mode := .Options.StringOr "mode" "default" }} {{ if $isDetailed }}
{{ $dlSpeed := $transfer.JSON.Float "dl_info_speed" }} {{ if eq $mode "upload" }}
{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}
{{ else }} {{ if lt $dlSpeed 1048576.0 }}
{{ printf "%.0f KiB/s" (div $dlSpeed 1024.0) }}
{{ else }}
{{ printf "%.1f MiB/s" (div $dlSpeed 1048576.0) }}
{{ end }} {{ end }}
DOWNLOADING
{{ if eq $mode "upload" }}
{{ $ulSpeed := $transfer.JSON.Float "up_info_speed" }}
{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}
UPLOADING
{{ end }}
{{ len ($seeding.JSON.Array "") }}
SEEDING
{{ if eq $mode "default" }}
{{ len ($leeching.JSON.Array "") }}
LEECHING
{{ end }}
{{ $downloadingTorrents := $leeching.JSON.Array "" }} {{ if gt (len $downloadingTorrents) 0 }}
{{ end }} {{ if eq $mode "upload" }} {{ $seedingTorrents := $seeding.JSON.Array "" }} {{ if gt (len $seedingTorrents) 0 }}
{{ end }} {{ end }}
{{ else }}
{{ $dlSpeed := $transfer.JSON.Float "dl_info_speed" }}
{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}
DOWNLOADING
{{ if eq $mode "upload" }}
{{ $ulSpeed := $transfer.JSON.Float "up_info_speed" }}
{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}
UPLOADING
{{ end }}
{{ len ($seeding.JSON.Array "") }}
SEEDING
{{ if eq $mode "default" }}
{{ len ($leeching.JSON.Array "") }}
LEECHING
{{ end }}
{{ end }} {{ else }}

Error fetching qBittorrent data.

Check QUI proxy URL and token.

{{ end }} - type: custom-api title: Dockhand title-url: https://dockhand.${GROUND_DOMAIN} cache: 1h url: https://dockhand.${GROUND_DOMAIN}/api/environments?env=1 headers: Authorization: Bearer ${DOCKHAND_API_TOKEN} subrequests: containers: url: https://dockhand.${GROUND_DOMAIN}/api/containers?env=1 headers: Authorization: Bearer ${DOCKHAND_API_TOKEN} images: url: https://dockhand.${GROUND_DOMAIN}/api/images?env=1 headers: Authorization: Bearer ${DOCKHAND_API_TOKEN} networks: url: https://dockhand.${GROUND_DOMAIN}/api/networks?env=1 headers: Authorization: Bearer ${DOCKHAND_API_TOKEN} volumes: url: https://dockhand.${GROUND_DOMAIN}/api/volumes?env=1 headers: Authorization: Bearer ${DOCKHAND_API_TOKEN} stacks: url: https://dockhand.${GROUND_DOMAIN}/api/stacks?env=1 headers: Authorization: Bearer ${DOCKHAND_API_TOKEN} template: | {{ $containers := (.Subrequest "containers").JSON.Array "" }} {{ $images := (.Subrequest "images").JSON.Array "" }} {{ $networks := (.Subrequest "networks").JSON.Array "" }} {{ $volumes := (.Subrequest "volumes").JSON.Array "" }} {{ $stacks := (.Subrequest "stacks").JSON.Array "" }} {{ $running := 0 }} {{ $stopped := 0 }} {{ range $containers }} {{ if eq (.String "state") "running" }} {{ $running = add $running 1 }} {{ else }} {{ $stopped = add $stopped 1 }} {{ end }} {{ end }}
{{ $running }}
{{ $stopped }}
{{ len $images }}
Images
{{ $unused := 0 }} {{ $totalBytes := 0 }} {{ range $images }} {{ if eq (.Int "containers") 0 }} {{ $unused = add $unused 1 }} {{ end }} {{ $totalBytes = add $totalBytes (.Int "size") }} {{ end }} {{ $totalGB := div $totalBytes 1073741824 }} {{ $decimal := mod (div $totalBytes 1048576) 1024 }}
Unused images:
{{ $unused }}
Total size:
{{ printf "%d.%02d GB" $totalGB $decimal }}
{{ len $volumes }}
Networks
{{ $userDefined := 0 }} {{ $system := 0 }} {{ range $networks }} {{ if gt (len (.Array "containers")) 0 }} {{ $userDefined = add $userDefined 1 }} {{ else }} {{ $system = add $system 1 }} {{ end }} {{ end }}
In use:
{{ $userDefined }}
Unused:
{{ $system }}
{{ len $networks }}
{{ len $stacks }}
- size: full widgets: - type: split-column widgets: - type: custom-api title: Indexers cache: 1m options: url: https://prowlarr.${GROUND_DOMAIN} base-url: https://prowlarr.${GROUND_DOMAIN} api-key: ${PROWLARR_API_KEY} collapse-after: 999 template: | {{ $apiBaseUrl := .Options.StringOr "base-url" "" }} {{ $key := .Options.StringOr "api-key" "" }} {{ $url := .Options.StringOr "url" "" }} {{ $collapseAfter := .Options.IntOr "collapse-after" 5 }} {{ if or (eq $apiBaseUrl "") (eq $key "") (eq $url "") }}
ERROR

Some options are not set or malformed.

{{ else }} {{ $indexUrl := printf "%s/api/v1/indexer" $apiBaseUrl }} {{ $indexData := newRequest $indexUrl | withHeader "Accept" "application/json" | withHeader "Authorization" "Basic ${AUTHENTIK_BASIC_AUTH}" | withHeader "X-Api-Key" $key | getResponse }} {{ if eq $indexData.Response.StatusCode 200 }} {{ else }}

Failed to fetch data (Status: {{ $indexData.Response.StatusCode }})

{{ end }} {{ end }} - type: custom-api title: Server Stats cache: 5m options: base-url: https://beszel.${GROUND_DOMAIN} email: ${BESZEL_EMAIL} password: ${BESZEL_PASSWORD} template: | {{ $baseURL := .Options.StringOr "base-url" "" }} {{ $email := .Options.StringOr "email" "" }} {{ $password := .Options.StringOr "password" "" }} {{ define "errorMsg" }}
ERROR

{{ . }}

{{ end }} {{ define "formatGigabytes" }} {{ $value := . }} {{ $label := "GiB" }} {{- if lt $value 1.0 }} {{ $value = mul $value 1024.0 }} {{ $label = "MiB" }} {{- else if lt $value 1024.0 }} {{ else }} {{ $value = div $value 1024.0 }} {{ $label = "TiB" }} {{ end }} {{ printf "%.1f" $value }} {{ $label }} {{ end }} {{ if or (eq $baseURL "") (eq $email "") (eq $password "") }} {{ template "errorMsg" "Some required options are not set." }} {{ else }} {{ $authBody := printf "{\"identity\":%s,\"password\":%s}" (.Options.JSON "email") (.Options.JSON "password") }} {{ $authResponse := newRequest (print $baseURL "/api/collections/users/auth-with-password") | withParameter "fields" "token" | withHeader "Content-Type" "application/json" | withStringBody $authBody | getResponse }} {{ if ne $authResponse.Response.StatusCode 200 }} {{ template "errorMsg" (printf "Beszel authentication failed: %s" $authResponse.Response.Status) }} {{ else }} {{ $token := concat "Bearer " ($authResponse.JSON.String "token") }} {{ $systemsResponse := newRequest (print $baseURL "/api/collections/systems/records") | withHeader "Authorization" $token | getResponse }} {{ if ne $systemsResponse.Response.StatusCode 200 }} {{ template "errorMsg" (printf "Failed to fetch Beszel systems: %s" $systemsResponse.Response.Status) }} {{ else }} {{ range $n, $system := $systemsResponse.JSON.Array "items" }} {{ $status := $system.String "status" }} {{ $systemStatsRequest := newRequest (print $baseURL "/api/collections/system_stats/records") | withHeader "Authorization" $token | withParameter "sort" "-created" | withParameter "page" "1" | withParameter "perPage" "1" | withParameter "filter" (print "type='1m'&&system='" ($system.String "id") "'") | getResponse }} {{ $systemStatItems := $systemStatsRequest.JSON.Array "items" }} {{ $hostname := $system.String "name" }} {{ $uptimeSec := $system.Float "info.u" }} {{ $systemTemp := $system.Float "info.dt" }} {{ $cpuLoad := $system.Float "info.cpu" }} {{ $cpuLoad1m := $system.Float "info.la.0" }} {{ $cpuLoad15m := $system.Float "info.la.2" }} {{ $memoryUsedPercent := $system.Float "info.mp" }} {{ $rootUsedPercent := $system.Float "info.dp" }} {{ $hasStats := false }} {{ $systemStats := "" }} {{ $memoryTotalGb := 0.0 }} {{ $memoryUsedGb := 0.0 }} {{ $swapTotalGb := 0.0 }} {{ $swapUsedGb := 0.0 }} {{ $swapUsedPercent := 0.0 }} {{ $rootTotalGb := 0.0 }} {{ $rootUsedGb := 0.0 }} {{ if gt (len $systemStatItems) 0 }} {{ $hasStats = true }} {{ $systemStats = index $systemStatItems 0 }} {{ $memoryTotalGb = $systemStats.Float "stats.m" }} {{ $memoryUsedGb = $systemStats.Float "stats.mu" }} {{ $swapTotalGb = $systemStats.Float "stats.s" }} {{ $swapUsedGb = $systemStats.Float "stats.su" }} {{ if gt $swapTotalGb 0.0 }} {{ $swapUsedPercent = mul (div $swapUsedGb $swapTotalGb) 100.0 }} {{ end }} {{ $rootTotalGb = $systemStats.Float "stats.d" }} {{ $rootUsedGb = $systemStats.Float "stats.du" }} {{ end }}
{{ $hostname }}
{{ if eq $status "up" }} {{ printf "%.1f" (mul $uptimeSec 0.000011574) }}d uptime {{ else }} unreachable {{ end }}
{{- if eq $status "up" }}
Kernel
{{ $system.String "info.k" }}
CPU
{{ $system.String "info.m" }}
{{- end }}
CPU
{{- if ge $systemTemp 80.0 }} {{- end }}
{{ $cpuLoad }} %
1M AVG
{{ printf "%.1f" $cpuLoad1m }} %
15M AVG
{{ printf "%.1f" $cpuLoad15m }} %
TEMP C
{{ printf "%.1f" $systemTemp }} C
RAM
{{ $memoryUsedPercent }} %
{{- if $hasStats }}
RAM
{{ template "formatGigabytes" $memoryUsedGb }} / {{ template "formatGigabytes" $memoryTotalGb }}
{{- if gt $swapTotalGb 0.0 }}
SWAP
{{ template "formatGigabytes" $swapUsedGb }} / {{ template "formatGigabytes" $swapTotalGb }}
{{- end }}
{{- end }}
{{- if gt $swapTotalGb 0.0 }}
{{- end }}
DISK
{{ $rootUsedPercent }} %
{{- if $hasStats }}
  • /
    {{ template "formatGigabytes" $rootUsedGb }} / {{ template "formatGigabytes" $rootTotalGb }}
  • {{ range $key, $efs := ($systemStats.Get "stats.efs").Map }}
  • {{ $key }}
    {{ template "formatGigabytes" (($efs.Get "du").Float) }} / {{ template "formatGigabytes" (($efs.Get "d").Float) }}
  • {{ end }}
{{- end }}
{{ range $key, $efs := ($system.Get "info.efs").Map }} {{ $percent := $efs.Float }}
{{ end }}
{{ end }} {{ end }} {{ end }} {{ end }} - type: custom-api title: Immich Stats cache: 10m url: https://immich.${GROUND_DOMAIN}/api/server/statistics headers: x-api-key: ${IMMICH_API_KEY} subrequests: storage: url: https://immich.${GROUND_DOMAIN}/api/server/storage headers: x-api-key: ${IMMICH_API_KEY} template: | {{ $photos := .JSON.Int "photos" }} {{ $videos := .JSON.Int "videos" }} {{ $usageGiB := div (toFloat (.JSON.Int "usage")) 1073741824.0 }} {{ $storage := .Subrequest "storage" }} {{ $storageOK := and (ge $storage.Response.StatusCode 200) (le $storage.Response.StatusCode 299) }} {{ $totalGiB := 0.0 }} {{ $percentage := 0.0 }} {{ if $storageOK }} {{ $totalGiB = div (toFloat ($storage.JSON.Int "diskSizeRaw")) 1073741824.0 }} {{ $percentage = $storage.JSON.Float "diskUsagePercentage" }} {{ end }}
{{ $photos | formatNumber }}
PHOTOS
{{ $videos | formatNumber }}
VIDEOS
{{ printf "%.1f" $usageGiB }} GiB
STORAGE
{{ if $storageOK }}
{{ printf "%.1f" $usageGiB }} / {{ printf "%.1f" $totalGiB }} GiB {{ printf "%.1f" $percentage }}%
{{ else }}
Storage total unavailable.
{{ end }} - size: small widgets: - type: custom-api cache: 1h title: Internet Speed (Euclid) title-url: https://speedtest.${GROUND_DOMAIN} url: https://speedtest.${GROUND_DOMAIN}/api/v1/results/latest headers: Authorization: Bearer ${SPEEDTEST_API_TOKEN} Accept: application/json subrequests: stats: url: https://speedtest.${GROUND_DOMAIN}/api/v1/stats headers: Authorization: Bearer ${SPEEDTEST_API_TOKEN} Accept: application/json options: showPercentageDiff: true template: | {{ $showPercentage := .Options.BoolOr "showPercentageDiff" true }} {{ $stats := .Subrequest "stats" }}
{{ $downloadChange := percentChange (.JSON.Float "data.download_bits") ($stats.JSON.Float "data.download.avg_bits") }} {{ if $showPercentage }}
{{ $downloadChange | printf "%+.1f%%" }}
{{ end }}
{{ .JSON.Float "data.download_bits" | mul 0.000001 | printf "%.1f" }}
DOWNLOAD
{{ $uploadChange := percentChange (.JSON.Float "data.upload_bits") ($stats.JSON.Float "data.upload.avg_bits") }} {{ if $showPercentage }}
{{ $uploadChange | printf "%+.1f%%" }}
{{ end }}
{{ .JSON.Float "data.upload_bits" | mul 0.000001 | printf "%.1f" }}
UPLOAD
{{ $pingChange := percentChange (.JSON.Float "data.ping") ($stats.JSON.Float "data.ping.avg") }} {{ if $showPercentage }}
{{ $pingChange | printf "%+.1f%%" }}
{{ end }}
{{ .JSON.Float "data.ping" | printf "%.0f ms" }}
PING