#!/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"