Files
2026-04-11 09:45:12 -05:00

286 lines
17 KiB
Bash

#!/bin/bash
# UniFi Network Dashboard - Comprehensive overview
# Usage: bash dashboard.sh [json]
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/unifi-api.sh"
OUTPUT_FILE="$HOME/clawd/memory/bank/unifi-inventory.md"
# Disable strict mode for dashboard (we handle errors gracefully)
set +e
OUTPUT_JSON="${1:-}"
# Ensure output directory exists
mkdir -p "$(dirname "$OUTPUT_FILE")"
# Create shared session for all requests
export UNIFI_COOKIE_FILE=$(mktemp)
trap "rm -f '$UNIFI_COOKIE_FILE'" EXIT
unifi_login "$UNIFI_COOKIE_FILE"
# Fetch all data upfront (using full API path format)
HEALTH=$(unifi_get "/api/s/$UNIFI_SITE/stat/health")
DEVICES=$(unifi_get "/api/s/$UNIFI_SITE/stat/device")
CLIENTS=$(unifi_get "/api/s/$UNIFI_SITE/stat/sta")
PORTFWD=$(unifi_get "/api/s/$UNIFI_SITE/rest/portforward")
FWRULES=$(unifi_get "/api/s/$UNIFI_SITE/rest/firewallrule")
NETWORKS=$(unifi_get "/api/s/$UNIFI_SITE/rest/networkconf")
WLANS=$(unifi_get "/api/s/$UNIFI_SITE/rest/wlanconf")
ALARMS=$(unifi_get "/api/s/$UNIFI_SITE/stat/alarm")
ROUTING=$(unifi_get "/api/s/$UNIFI_SITE/stat/routing")
SYSINFO=$(unifi_get "/api/s/$UNIFI_SITE/stat/sysinfo")
# Debug: Dump all JSON to a file for troubleshooting
jq -n \
--argjson health "$HEALTH" \
--argjson devices "$DEVICES" \
--argjson clients "$CLIENTS" \
--argjson portforward "$PORTFWD" \
--argjson networks "$NETWORKS" \
--argjson wlans "$WLANS" \
'{health: $health, devices: $devices, clients: $clients, networks: $networks, wlans: $wlans}' \
> dashboard_debug_dump.json 2>/dev/null
if [ "$OUTPUT_JSON" = "json" ]; then
jq -n \
--argjson health "$HEALTH" \
--argjson devices "$DEVICES" \
--argjson clients "$CLIENTS" \
--argjson portforward "$PORTFWD" \
--argjson firewall "$FWRULES" \
--argjson networks "$NETWORKS" \
--argjson wlans "$WLANS" \
--argjson alarms "$ALARMS" \
--argjson routing "$ROUTING" \
--argjson sysinfo "$SYSINFO" \
'{health: $health, devices: $devices, clients: $clients, portforward: $portforward, firewall: $firewall, networks: $networks, wlans: $wlans, alarms: $alarms, routing: $routing, sysinfo: $sysinfo}'
exit 0
fi
# Generate dashboard output and save to file
{
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ UNIFI NETWORK DASHBOARD ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
# System Info
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ SYSTEM INFO │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo "$SYSINFO" | jq -r '
.data[0] |
"Version: \(.version // "N/A") | Hostname: \(.hostname // "N/A") | Timezone: \(.timezone // "N/A")"
' 2>/dev/null || echo " Unable to fetch system info"
echo ""
# Health Overview
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ HEALTH STATUS │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
echo "$HEALTH" | jq -r '
.data[] |
"\(.subsystem | ascii_upcase): \(if .status == "ok" then "OK" else "\(.status)" end) | Gateways: \(.gw_mac // "N/A") | Clients: \(.num_user // 0)"
' 2>/dev/null | head -10
echo ""
# Devices
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ UNIFI DEVICES │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
printf "%-20s %-10s %-15s %-8s %-10s %-8s\n" "NAME" "MODEL" "IP" "STATE" "UPTIME" "CLIENTS"
printf "%-20s %-10s %-15s %-8s %-10s %-8s\n" "----" "-----" "--" "-----" "------" "-------"
echo "$DEVICES" | jq -r '
.data[] |
[
((.name // .hostname // .mac)[:20]),
((.model // "N/A")[:10]),
((.ip // "N/A")[:15]),
(if .state == 1 then "OK" else "FAIL" end),
(((.uptime // 0) / 3600 | floor | tostring + "h")),
((.num_sta // 0 | tostring))
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r name model ip state uptime clients; do
printf "%-20s %-10s %-15s %-8s %-10s %-8s\n" "$name" "$model" "$ip" "$state" "$uptime" "$clients"
done
echo ""
# Client Summary
TOTAL_CLIENTS=$(echo "$CLIENTS" | jq '.data | length' 2>/dev/null || echo 0)
WIRED_CLIENTS=$(echo "$CLIENTS" | jq '[.data[] | select(.is_wired == true)] | length' 2>/dev/null || echo 0)
WIFI_CLIENTS=$(echo "$CLIENTS" | jq '[.data[] | select(.is_wired == false)] | length' 2>/dev/null || echo 0)
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ CLIENTS: $TOTAL_CLIENTS total ($WIRED_CLIENTS wired, $WIFI_CLIENTS wireless) │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
printf "%-25s %-15s %-18s %-10s %-12s\n" "HOSTNAME" "IP" "MAC" "TYPE" "TX/RX MB/s"
printf "%-25s %-15s %-18s %-10s %-12s\n" "--------" "--" "---" "----" "----------"
echo "$CLIENTS" | jq -r '
.data | sort_by(-(.["wired-rx_bytes"] // .rx_bytes // 0)) | .[0:15] | .[] |
[
((.name // .hostname // .mac // "Unknown") | tostring | .[:25]),
((.ip // .last_ip // "N/A") | tostring | .[:15]),
((.mac // "N/A") | tostring | .[:18]),
(if .is_wired == true then "Wired" else "WiFi" end),
"\( ((.["tx_bytes-r"] // .["wired-tx_bytes-r"] // 0) / 1000000 * 10 | floor / 10) )/\( ((.["rx_bytes-r"] // .["wired-rx_bytes-r"] // 0) / 1000000 * 10 | floor / 10) )"
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r hostname ip mac type rate; do
printf "%-25s %-15s %-18s %-10s %-12s\n" "$hostname" "$ip" "$mac" "$type" "$rate"
done || echo " (error parsing client data)"
echo " (showing top 15 by traffic)"
echo ""
# Networks
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ NETWORKS │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
printf "%-25s %-8s %-18s %-10s %-15s\n" "NAME" "VLAN" "SUBNET" "PURPOSE" "DHCP"
printf "%-25s %-8s %-18s %-10s %-15s\n" "----" "----" "------" "-------" "----"
# Check if networks returned an error
if echo "$NETWORKS" | jq -e '.error' >/dev/null 2>&1; then
echo " (API returned 401 - REST endpoints may require additional permissions)"
else
echo "$NETWORKS" | jq -r '
.data[] |
[
((.name // "N/A") | tostring | .[:25]),
((.vlan // "-") | tostring | .[:8]),
((.ip_subnet // "N/A") | tostring | .[:18]),
((.purpose // "N/A") | tostring | .[:10]),
(if .dhcpd_enabled == true then "Enabled" else "Disabled" end)
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r name vlan subnet purpose dhcp; do
printf "%-25s %-8s %-18s %-10s %-15s\n" "$name" "$vlan" "$subnet" "$purpose" "$dhcp"
done
fi
echo ""
# WLANs
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ WIRELESS NETWORKS │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
printf "%-30s %-10s %-15s %-10s\n" "SSID" "ENABLED" "SECURITY" "BAND"
printf "%-30s %-10s %-15s %-10s\n" "----" "-------" "--------" "----"
# Check if wlans returned an error
if echo "$WLANS" | jq -e '.error' >/dev/null 2>&1; then
echo " (API returned 401 - REST endpoints may require additional permissions)"
else
echo "$WLANS" | jq -r '
.data[] |
[
((.name // "N/A") | tostring | .[:30]),
(if .enabled == true then "YES" else "NO" end),
((.security // "open") | tostring | .[:15]),
((if .wlan_band then .wlan_band else "both" end) | tostring | .[:10])
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r ssid enabled security band; do
printf "%-30s %-10s %-15s %-10s\n" "$ssid" "$enabled" "$security" "$band"
done
fi
echo ""
# Port Forwards
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ PORT FORWARDS │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
PF_COUNT=$(echo "$PORTFWD" | jq '.data | length' 2>/dev/null || echo 0)
if [ "$PF_COUNT" -eq 0 ]; then
echo " No port forwards configured"
else
printf "%-25s %-10s %-8s %-15s %-8s %-10s\n" "NAME" "ENABLED" "PROTO" "FWD TO" "PORT" "SRC PORT"
printf "%-25s %-10s %-8s %-15s %-8s %-10s\n" "----" "-------" "-----" "------" "----" "--------"
echo "$PORTFWD" | jq -r '
.data[] |
[
((.name // "N/A") | tostring | .[:25]),
(if .enabled == true then "YES" else "NO" end),
((.proto // "tcp") | tostring | .[:8]),
((.fwd // "N/A") | tostring | .[:15]),
((.fwd_port // "-") | tostring | .[:8]),
((.src_port // .dst_port // "-") | tostring | .[:10])
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r name enabled proto fwd port srcport; do
printf "%-25s %-10s %-8s %-15s %-8s %-10s\n" "$name" "$enabled" "$proto" "$fwd" "$port" "$srcport"
done
fi
echo ""
# Firewall Rules
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ FIREWALL RULES │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
FW_COUNT=$(echo "$FWRULES" | jq '.data | length' 2>/dev/null || echo 0)
if [ "$FW_COUNT" -eq 0 ]; then
echo " No custom firewall rules configured"
else
printf "%-25s %-10s %-10s %-10s %-10s %-10s\n" "NAME" "ENABLED" "ACTION" "PROTO" "RULESET" "INDEX"
printf "%-25s %-10s %-10s %-10s %-10s %-10s\n" "----" "-------" "------" "-----" "-------" "-----"
echo "$FWRULES" | jq -r '
.data | sort_by(.rule_index) | .[] |
[
((.name // "N/A") | tostring | .[:25]),
(if .enabled == true then "YES" else "NO" end),
((.action // "N/A") | tostring | .[:10]),
((.protocol // "all") | tostring | .[:10]),
((.ruleset // "N/A") | tostring | .[:10]),
((.rule_index // 0) | tostring)
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r name enabled action proto ruleset idx; do
printf "%-25s %-10s %-10s %-10s %-10s %-10s\n" "$name" "$enabled" "$action" "$proto" "$ruleset" "$idx"
done
fi
echo ""
# Static Routes
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ ROUTES │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
ROUTE_COUNT=$(echo "$ROUTING" | jq '.data | length' 2>/dev/null || echo 0)
if [ "$ROUTE_COUNT" -eq 0 ]; then
echo " No static routes (using default)"
else
printf "%-20s %-18s %-15s %-12s %-10s\n" "NAME" "DESTINATION" "NEXT HOP" "INTERFACE" "METRIC"
printf "%-20s %-18s %-15s %-12s %-10s\n" "----" "-----------" "--------" "---------" "------"
echo "$ROUTING" | jq -r '
.data[] |
[
((.name // .pfx // "N/A") | tostring | .[:20]),
((.pfx // "N/A") | tostring | .[:18]),
((.nh[0].t // .nh[0].gw // "N/A") | tostring | .[:15]),
((.nh[0].intf_name // "N/A") | tostring | .[:12]),
((.metric // 0) | tostring)
] | @tsv
' 2>/dev/null | head -10 | while IFS=$'\t' read -r name dest nexthop intf metric; do
printf "%-20s %-18s %-15s %-12s %-10s\n" "$name" "$dest" "$nexthop" "$intf" "$metric"
done
fi
echo ""
# Recent Alarms
echo "┌─────────────────────────────────────────────────────────────────────────────┐"
echo "│ RECENT ALARMS (last 10) │"
echo "└─────────────────────────────────────────────────────────────────────────────┘"
ALARM_COUNT=$(echo "$ALARMS" | jq '.data | length' 2>/dev/null || echo 0)
if [ "$ALARM_COUNT" -eq 0 ]; then
echo " No recent alarms"
else
printf "%-20s %-50s\n" "TIME" "MESSAGE"
printf "%-20s %-50s\n" "----" "-------"
echo "$ALARMS" | jq -r '
.data | sort_by(-.time) | .[0:10] | .[] |
[
((.datetime // (.time | todate? // "N/A"))[:20]),
((.msg // .key // "N/A")[:50])
] | @tsv
' 2>/dev/null | while IFS=$'\t' read -r time msg; do
printf "%-20s %-50s\n" "$time" "$msg"
done
fi
echo ""
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ Dashboard generated at $(date '+%Y-%m-%d %H:%M:%S')"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
} | tee "$OUTPUT_FILE"