{config, ...}: { services.glance = { enable = true; openFirewall = false; settings = { server = { host = "0.0.0.0"; }; pages = [ { name = "Home"; columns = [ { size = "small"; widgets = [ { type = "calendar"; first-day-of-week = "monday"; } { type = "twitch-channels"; channels = [ "theprimeagen" ]; } { type = "rss"; title = "Rss Feeds"; cache = "1h"; feeds = [ { url = "https://github.com/NixOS/nixpkgs/commits/nixpkgs-unstable.atom"; title = "Nixpkgs Unstable"; } ]; } ]; } { size = "full"; widgets = [ { type = "group"; widgets = [ {type = "hacker-news";} {type = "lobsters";} ]; } { type = "server-stats"; servers = [ { type = "local"; name = "Tyr"; } ]; } { type = "custom-api"; title = "Beszel stats"; cache = "5m"; options = { base-url = "\${BESZEL_URL}"; api-key = "\${BESZEL_TOKEN}"; }; template = '' {{/* Required config options */}} {{ $baseURL := .Options.StringOr "base-url" "" }} {{ $apiKey := .Options.StringOr "api-key" "" }} {{/* Error message template */}} {{ define "errorMsg" }}
ERROR

{{ . }}

{{ end }} {{ define "formatGigabytes" }} {{ $value := . }} {{ $label := "GB" }} {{- if lt $value 1.0 }} {{ $value = mul $value 1000.0 }} {{ $label = "MB" }} {{- else if lt $value 1000.0 }} {{ else }} {{ $value = div $value 1000.0 }} {{ $label = "TB" }} {{ end }} {{ printf "%.1f" $value }} {{ $label }} {{ end }} {{/* Check required fields */}} {{ if or (eq $baseURL "") (eq $apiKey "") }} {{ template "errorMsg" "Some required options are not set." }} {{ else }} {{ $token := concat "Bearer " $apiKey }} {{ $systemsResponse := newRequest (print $baseURL "/api/collections/systems/records") | withHeader "Authorization" $token | getResponse }} {{ $systems := $systemsResponse.JSON.Array "items" }} {{ range $n, $system := $systems }} {{ $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 }} {{ $systemStats := index ($systemStatsRequest.JSON.Array "items") 0 }} {{ $hostname := $system.String "name" }} {{ $uptimeSec := $system.Float "info.u" }} {{ $systemTemp := $system.Float "info.dt"}} {{ $cpuLoad := $system.Float "info.cpu" }} {{ $cpuLoad1m := $system.Float "info.l1" }} {{ $cpuLoad15m := $system.Float "info.l15" }} {{ $memoryUsedPercent := $system.Float "info.mp" }} {{ $memoryTotalGb := $systemStats.Float "stats.m" }} {{ $memoryUsedGb := $systemStats.Float "stats.mu" }} {{ $swapTotalGb := $systemStats.Float "stats.s" }} {{ $swapUsedGb := $systemStats.Float "stats.su" }} {{ $swapUsedPercent := mul (div $swapUsedGb $swapTotalGb) 100.0 }} {{ $rootUsedPercent := $system.Float "info.dp" }} {{ $rootTotalGb := $systemStats.Float "stats.d" }} {{ $rootUsedGb := $systemStats.Float "stats.du" }}
{{ $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 }} °
RAM
{{ $memoryUsedPercent }} %
RAM
{{ template "formatGigabytes" $memoryUsedGb }} / {{ template "formatGigabytes" $memoryTotalGb }}
{{- if gt $swapTotalGb 0.0 }}
SWAP
{{ template "formatGigabytes" $swapUsedGb }} / {{ template "formatGigabytes" $swapTotalGb }}
{{- end }}
{{- if gt $swapTotalGb 0.0 }}
{{- end }}
DISK
{{ $rootUsedPercent }} %
  • /
    {{ 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 }}
{{ range $key, $efs := ($systemStats.Get "stats.efs").Map }} {{ $efsTotalGb := (($efs.Get "d").Float) }} {{ $efsUsedGb := (($efs.Get "du").Float) }} {{ $efsPercent := mul (div $efsUsedGb $efsTotalGb) 100 }}
{{ end }}
{{ end }} {{ end }} ''; } ]; } { size = "small"; widgets = [ { type = "weather"; units = "metric"; hour-format = "24h"; location = "Otočac, Hrvatska"; } { type = "releases"; cache = "1d"; repositories = [ "glanceapp/glance" "immich-app/immich" "syncthing/syncthing" ]; } { type = "custom-api"; title = "GitHub Notifications"; url = "https://api.github.com/notifications?all=true&per_page=20"; headers = { Authorization = "Bearer \${GITHUB_TOKEN}"; Accept = "application/vnd.github+json"; }; template = '' ''; } ]; } ]; } { name = "Gaming"; columns = [ { size = "small"; widgets = [ { type = "twitch-top-games"; limit = 20; collapse-after = 13; exclude = [ "just-chatting" "pools-hot-tubs-and-beaches" "music" "art" "asmr" ]; } ]; } { size = "full"; widgets = [ { type = "group"; widgets = [ { type = "reddit"; show-thumbnails = true; subreddit = "pcgaming"; } { type = "reddit"; subreddit = "games"; } ]; } { type = "videos"; style = "grid-cards"; collapse-after-rows = 3; channels = [ "UCNvzD7Z-g64bPXxGzaQaa4g" # GameRanx "UCZ7AeeVbyslLM_8-nVy2B8Q" # Skill Up "UCHDxYLv8iovIbhrfl16CNyg" # GameLinked "UC9PBzalIcEQCsiIkq36PyUA" # Digital Foundry ]; } ]; } { size = "small"; widgets = [ { type = "reddit"; subreddit = "gamingnews"; limit = 7; style = "vertical-cards"; } ]; } ]; } { name = "SelfHosted Services"; columns = [ { size = "small"; widgets = [ { type = "custom-api"; title = "Audiobookshelf"; title-url = "\${AUDIOBOOKSHELF_URL}"; options = { base-url = "\${AUDIOBOOKSHELF_URL}"; api-key = "\${AUDIOBOOKSHELF_KEY}"; }; cache = "5m"; template = '' {{ $baseURL := .Options.StringOr "base-url" "" }} {{ $apiKey := .Options.StringOr "api-key" "" }} {{ define "errorMsg" }}
ERROR

{{ . }}

{{ end }} {{ $bearer := printf "Bearer %s" $apiKey }} {{ $librariesRequestURL := concat $baseURL "/api/libraries" }} {{ $librariesResponse := newRequest $librariesRequestURL | withHeader "Content-Type" "application/json" | withHeader "Authorization" $bearer | getResponse }} {{ if $librariesResponse.JSON.Exists "libraries" }} {{ $all_libraries := $librariesResponse.JSON.Array "libraries" }} {{ $books_count := 0 }} {{ $books_duration := 0 }} {{ $podcasts_count := 0 }} {{ $podcasts_duration := 0 }} {{ range $library := $all_libraries }} {{ $lib_id := $library.String "id" }} {{ $lib_request_url := concat $baseURL "/api/libraries/" $lib_id "/stats"}} {{ $lib_stats := newRequest $lib_request_url | withHeader "Content-Type" "application/json" | withHeader "Authorization" $bearer | getResponse }} {{ $lib_type := $library.String "mediaType" }} {{ $lib_item_count := $lib_stats.JSON.Int "totalItems" }} {{ $lib_total_duration := $lib_stats.JSON.Int "totalDuration" }} {{ if eq $lib_type "book" }} {{ $books_count = add $books_count $lib_item_count }} {{ $books_duration = add $books_duration $lib_total_duration }} {{ else if eq $lib_type "podcast" }} {{ $podcasts_count = add $podcasts_count $lib_item_count }} {{ $podcasts_duration = add $podcasts_duration $lib_total_duration }} {{ end }} {{ end }} {{ $books_duration = duration (concat (printf "%d" $books_duration) "s") }} {{ $podcasts_duration = duration (concat (printf "%d" $podcasts_duration) "s") }}
{{ $books_count }}
Books
{{ $books_duration }}
Duration
{{ $podcasts_count }}
Podcasts
{{ $podcasts_duration }}
Duration
{{ else }} {{ template "errorMsg" "Could not fetch data from API!" }} {{ end }} ''; } { type = "custom-api"; title = "Immich Stats"; cache = "1d"; url = "https://immich.cronyakatsuki.xyz/api/server/statistics"; headers = { x-api-key = "\${IMMICH_API_KEY}"; Accept = "application/json"; }; template = ''
{{ .JSON.Int "photos" | formatNumber }}
PHOTOS
{{ .JSON.Int "videos" | formatNumber }}
VIDEOS
{{ div (.JSON.Int "usage" | toFloat) 1073741824 | toInt | formatNumber }}GB
USAGE
''; } { type = "custom-api"; title = "Jellyfin Stats"; base-url = "\${JELLYFIN_URL}"; options = { url = "\${JELLYFIN_URL}"; key = "\${JELLYFIN_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 -}} {{- $requestUrl := printf "%s/emby/Items/Counts?api_key=%s" $url $key -}} {{- $jellyfinData := newRequest $requestUrl | getResponse -}} {{- if eq $jellyfinData.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
{{- else -}}

Failed: {{ $jellyfinData.Response.Status }}

{{- end -}} {{- end -}} ''; } ]; } { size = "full"; widgets = [ ]; } ]; } ]; }; }; systemd.services.glance.serviceConfig = { EnvironmentFile = ["${config.age.secrets.glance.path}"]; }; services.traefik.dynamicConfigOptions.http = { services.glance.loadBalancer.servers = [ { url = "http://localhost:8080"; } ]; routers.glance = { rule = "Host(`glance.home.cronyakatsuki.xyz`)"; tls = { certResolver = "porkbun"; }; service = "glance"; entrypoints = "websecure"; }; }; }