#!/usr/bin/env bash # # Homelab monitoring, Part 3: Ready-made dashboards, sorted into folders # https://www.zadran.net/guides/homelab-monitoring-3-dashboards-in-folders # Generated from the guide, last updated 2026-10-13. # # NO WARRANTY. This script is provided as is. You are responsible for # understanding what it does before running it, and for the consequences. # Take a backup first. See https://www.zadran.net/disclaimer # # Destructive steps are left commented out on purpose -- uncomment them # only once you have understood what they do. set -euo pipefail mkdir -p /opt/monitoring/grafana/provisioning/dashboards/files/{Servers,Network} cat > /opt/monitoring/grafana/provisioning/dashboards/dashboards.yaml <<'EOF' apiVersion: 1 providers: - name: homelab orgId: 1 type: file disableDeletion: true allowUiUpdates: false updateIntervalSeconds: 60 options: path: /etc/grafana/provisioning/dashboards/files foldersFromFilesStructure: true EOF fix_ds() { jq 'walk(if type == "string" then gsub("\\$\\{DS_PROMETHEUS\\}"; "prometheus") else . end) | del(.__inputs, .__requires, .__elements) | .id = null' "$1" } cd /tmp curl -fsSL -o 1860.json https://grafana.com/api/dashboards/1860/revisions/latest/download fix_ds 1860.json > /opt/monitoring/grafana/provisioning/dashboards/files/Servers/node-exporter-full.json curl -fsSL -o 10347.json https://grafana.com/api/dashboards/10347/revisions/4/download CPU='100 - avg(rate(node_cpu_seconds_total{mode="idle", instance=~"$instance:.*"}[1m])) * 100' fix_ds 10347.json | jq --arg cpu "$CPU" ' (.. | objects | select(.id? == 7 and has("targets"))) |= (.targets |= [.[0] | .expr = $cpu] | .fieldConfig.defaults.unit = "percent" | .fieldConfig.defaults.max = 100 | .fieldConfig.defaults.thresholds.steps[1].value = 70 | .fieldConfig.defaults.thresholds.steps[2].value = 90) | (.. | objects | select(.id? == 23 and has("targets"))) |= (.targets |= [.[0] | .expr = $cpu] | .fieldConfig.defaults.unit = "percent" | .fieldConfig.defaults.min = 0 | .fieldConfig.defaults.max = 100) | (.. | objects | select(.id? == 22 and has("targets"))) |= (.targets[0].expr = "count(count by (cpu) (node_cpu_seconds_total{instance=~\"$instance:.*\"}))") ' > /opt/monitoring/grafana/provisioning/dashboards/files/Servers/proxmox.json grep -c node_cpu_seconds_total /opt/monitoring/grafana/provisioning/dashboards/files/Servers/proxmox.json curl -fsSL -o /opt/monitoring/grafana/provisioning/dashboards/files/Servers/nas-pools.json \ https://www.zadran.net/downloads/grafana/dashboards/nas-pools.json D=/opt/monitoring/grafana/provisioning/dashboards/files/Network RAW=https://raw.githubusercontent.com/unpoller/dashboards/adbe342f09d0fe06b1cc152d28ab0bae5ee3d8b9/v2.0.0 for pair in "Client%20Insights:unifi-clients" "Network%20Sites:unifi-sites" "UAP%20Insights:unifi-uap" "USW%20Insights:unifi-usw"; do curl -fsSL -o "/tmp/${pair#*:}.json" "$RAW/UniFi-Poller_%20${pair%%:*}%20-%20Prometheus.json" fix_ds "/tmp/${pair#*:}.json" > "$D/${pair#*:}.json" done jq ' walk(if type == "object" and has("panels") then .panels |= map(select(.id != 32)) else . end) | (.. | objects | select(.id? == 48 and has("targets"))) |= (.targets[0].expr = "count(unpoller_client_uptime_seconds{site_name=~\"$Site\", ap_name=~\"$AP\", wired=\"false\"}) or vector(0)") | (.. | objects | select(.id? == 60 and has("targets"))) |= (.targets[0].expr = "count(unpoller_client_uptime_seconds{site_name=~\"$Site\", ap_name=~\"$AP\", wired=\"false\", essid=~\"(?i).*guest.*\"}) or vector(0)") | (.. | objects | select((.id? == 52 or .id? == 54 or .id? == 56) and has("targets"))) |= (.targets |= map(.instant = true | .range = false)) ' "$D/unifi-uap.json" > /tmp/uap.json && mv /tmp/uap.json "$D/unifi-uap.json" ls -1 $D cd /opt/monitoring && docker compose restart grafana sleep 20 set -a; . /opt/monitoring/.env; set +a curl -s -u "admin:$GRAFANA_ADMIN_PASSWORD" 'localhost:3000/api/search?type=dash-db' | jq -r '.[] | "\(.folderTitle)\t\(.title)"' | sort | column -t -s $'\t'