Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f287d1f
Improve error handling, observability, and robustness
claude Mar 19, 2026
0874fc1
Add comprehensive monitoring: alerts, security, traffic, events, DDNS…
claude Mar 19, 2026
e71d5f1
Harden new monitoring code: bound arrays, sanitize API key, clean up …
claude Mar 19, 2026
f1673e2
Address all security audit findings
claude Mar 19, 2026
07301af
Fix remaining security findings from re-audit (5 medium, 6 low)
claude Mar 19, 2026
6cfbad1
Fix final low-severity findings from third audit
claude Mar 19, 2026
8af6b3a
Bound remaining unbounded arrays and cap error counter
claude Mar 19, 2026
09c3b8c
Security hardening, UX improvements, and CI setup
darox Apr 15, 2026
bd1565b
Fix PR-Agent CI config and address code review findings
darox Apr 16, 2026
af30d83
Fix cert validation: reject expired self-signed certificates
darox Apr 16, 2026
f05ca5f
Add diagnostics, error detail, and update checker
darox Apr 16, 2026
2a12c0f
Rename scrollable to compact mode, fix expanded window height
darox Apr 16, 2026
cdb3377
Switch DPI to v2 traffic API, move diagnostics to Preferences
darox Apr 16, 2026
966ebda
Remove broken monitoring endpoints and dead code
darox Apr 16, 2026
0f3532f
Add configurable poll interval and enhance diagnostics
darox Apr 16, 2026
d084ca9
Remove live status fields from diagnostics, keep only logs
darox Apr 16, 2026
94f87aa
Remove WAN IP from diagnostics report
darox Apr 16, 2026
ccce434
Show update available in menu footer below last updated timestamp
darox Apr 16, 2026
d2fb3e9
Add 5-tap debug mode on version to toggle fake update indicator
darox Apr 16, 2026
093d5b9
Add Fake Update toggle in Diagnostics debug mode
darox Apr 16, 2026
5a176aa
Remove label from debug mode toggle
darox Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 33 additions & 20 deletions Scripts/compile_and_run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ APP_NAME="UniFiBar"
BUILD_DIR="$PROJECT_DIR/.build"
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"

# Source version info
source "$PROJECT_DIR/version.env"
# Source version info (with defaults)
APP_VERSION="0.0.0"
BUILD_NUMBER="0"
if [ -f "$PROJECT_DIR/version.env" ]; then
source "$PROJECT_DIR/version.env"
fi

echo "==> Killing existing $APP_NAME..."
pkill -x "$APP_NAME" 2>/dev/null || true
Expand Down Expand Up @@ -69,26 +73,35 @@ if [ -f "$PROJECT_DIR/UniFiBar.entitlements" ]; then
cp "$PROJECT_DIR/UniFiBar.entitlements" "$APP_BUNDLE/Contents/Resources/"
fi

# Generate .icns from app icon PNGs
ICONSET_DIR="$BUILD_DIR/AppIcon.iconset"
rm -rf "$ICONSET_DIR"
mkdir -p "$ICONSET_DIR"
# Generate .icns from app icon PNGs (if available)
ICON_SRC="$PROJECT_DIR/Resources/Assets.xcassets/AppIcon.appiconset"
cp "$ICON_SRC/icon_16x16.png" "$ICONSET_DIR/icon_16x16.png"
cp "$ICON_SRC/icon_16x16@2x.png" "$ICONSET_DIR/icon_16x16@2x.png"
cp "$ICON_SRC/icon_32x32.png" "$ICONSET_DIR/icon_32x32.png"
cp "$ICON_SRC/icon_32x32@2x.png" "$ICONSET_DIR/icon_32x32@2x.png"
cp "$ICON_SRC/icon_128x128.png" "$ICONSET_DIR/icon_128x128.png"
cp "$ICON_SRC/icon_128x128@2x.png" "$ICONSET_DIR/icon_128x128@2x.png"
cp "$ICON_SRC/icon_256x256.png" "$ICONSET_DIR/icon_256x256.png"
cp "$ICON_SRC/icon_256x256@2x.png" "$ICONSET_DIR/icon_256x256@2x.png"
cp "$ICON_SRC/icon_512x512.png" "$ICONSET_DIR/icon_512x512.png"
cp "$ICON_SRC/icon_512x512@2x.png" "$ICONSET_DIR/icon_512x512@2x.png"
iconutil -c icns "$ICONSET_DIR" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 2>/dev/null || true
if [ -d "$ICON_SRC" ]; then
ICONSET_DIR="$BUILD_DIR/AppIcon.iconset"
rm -rf "$ICONSET_DIR"
mkdir -p "$ICONSET_DIR"
cp "$ICON_SRC/icon_16x16.png" "$ICONSET_DIR/icon_16x16.png"
cp "$ICON_SRC/icon_16x16@2x.png" "$ICONSET_DIR/icon_16x16@2x.png"
cp "$ICON_SRC/icon_32x32.png" "$ICONSET_DIR/icon_32x32.png"
cp "$ICON_SRC/icon_32x32@2x.png" "$ICONSET_DIR/icon_32x32@2x.png"
cp "$ICON_SRC/icon_128x128.png" "$ICONSET_DIR/icon_128x128.png"
cp "$ICON_SRC/icon_128x128@2x.png" "$ICONSET_DIR/icon_128x128@2x.png"
cp "$ICON_SRC/icon_256x256.png" "$ICONSET_DIR/icon_256x256.png"
cp "$ICON_SRC/icon_256x256@2x.png" "$ICONSET_DIR/icon_256x256@2x.png"
cp "$ICON_SRC/icon_512x512.png" "$ICONSET_DIR/icon_512x512.png"
cp "$ICON_SRC/icon_512x512@2x.png" "$ICONSET_DIR/icon_512x512@2x.png"
iconutil -c icns "$ICONSET_DIR" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 2>/dev/null || true
else
echo "WARNING: App icon assets not found at $ICON_SRC, skipping icon generation"
fi

# Copy status bar icon
cp "$PROJECT_DIR/Resources/Assets.xcassets/StatusBarIcon.imageset/icon@1x.png" "$APP_BUNDLE/Contents/Resources/"
cp "$PROJECT_DIR/Resources/Assets.xcassets/StatusBarIcon.imageset/icon@2x.png" "$APP_BUNDLE/Contents/Resources/"
# Copy status bar icon (if available)
STATUSBAR_SRC="$PROJECT_DIR/Resources/Assets.xcassets/StatusBarIcon.imageset"
if [ -d "$STATUSBAR_SRC" ]; then
cp "$STATUSBAR_SRC/icon@1x.png" "$APP_BUNDLE/Contents/Resources/"
cp "$STATUSBAR_SRC/icon@2x.png" "$APP_BUNDLE/Contents/Resources/"
else
echo "WARNING: Status bar icon assets not found, skipping"
fi

echo "==> Signing (ad-hoc)..."
codesign --force --sign - "$APP_BUNDLE" 2>/dev/null || true
Expand Down
53 changes: 33 additions & 20 deletions Scripts/package_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ APP_NAME="UniFiBar"
BUILD_DIR="$PROJECT_DIR/.build"
APP_BUNDLE="$BUILD_DIR/release/$APP_NAME.app"

# Source version info
source "$PROJECT_DIR/version.env"
# Source version info (with defaults)
APP_VERSION="0.0.0"
BUILD_NUMBER="0"
if [ -f "$PROJECT_DIR/version.env" ]; then
source "$PROJECT_DIR/version.env"
fi

echo "==> Building $APP_NAME (release)..."
cd "$PROJECT_DIR"
Expand Down Expand Up @@ -60,26 +64,35 @@ cat > "$APP_BUNDLE/Contents/Info.plist" << PLIST
</plist>
PLIST

# Generate .icns from app icon PNGs
ICONSET_DIR="$BUILD_DIR/AppIcon.iconset"
rm -rf "$ICONSET_DIR"
mkdir -p "$ICONSET_DIR"
# Generate .icns from app icon PNGs (if available)
ICON_SRC="$PROJECT_DIR/Resources/Assets.xcassets/AppIcon.appiconset"
cp "$ICON_SRC/icon_16x16.png" "$ICONSET_DIR/icon_16x16.png"
cp "$ICON_SRC/icon_16x16@2x.png" "$ICONSET_DIR/icon_16x16@2x.png"
cp "$ICON_SRC/icon_32x32.png" "$ICONSET_DIR/icon_32x32.png"
cp "$ICON_SRC/icon_32x32@2x.png" "$ICONSET_DIR/icon_32x32@2x.png"
cp "$ICON_SRC/icon_128x128.png" "$ICONSET_DIR/icon_128x128.png"
cp "$ICON_SRC/icon_128x128@2x.png" "$ICONSET_DIR/icon_128x128@2x.png"
cp "$ICON_SRC/icon_256x256.png" "$ICONSET_DIR/icon_256x256.png"
cp "$ICON_SRC/icon_256x256@2x.png" "$ICONSET_DIR/icon_256x256@2x.png"
cp "$ICON_SRC/icon_512x512.png" "$ICONSET_DIR/icon_512x512.png"
cp "$ICON_SRC/icon_512x512@2x.png" "$ICONSET_DIR/icon_512x512@2x.png"
iconutil -c icns "$ICONSET_DIR" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 2>/dev/null || true
if [ -d "$ICON_SRC" ]; then
ICONSET_DIR="$BUILD_DIR/AppIcon.iconset"
rm -rf "$ICONSET_DIR"
mkdir -p "$ICONSET_DIR"
cp "$ICON_SRC/icon_16x16.png" "$ICONSET_DIR/icon_16x16.png"
cp "$ICON_SRC/icon_16x16@2x.png" "$ICONSET_DIR/icon_16x16@2x.png"
cp "$ICON_SRC/icon_32x32.png" "$ICONSET_DIR/icon_32x32.png"
cp "$ICON_SRC/icon_32x32@2x.png" "$ICONSET_DIR/icon_32x32@2x.png"
cp "$ICON_SRC/icon_128x128.png" "$ICONSET_DIR/icon_128x128.png"
cp "$ICON_SRC/icon_128x128@2x.png" "$ICONSET_DIR/icon_128x128@2x.png"
cp "$ICON_SRC/icon_256x256.png" "$ICONSET_DIR/icon_256x256.png"
cp "$ICON_SRC/icon_256x256@2x.png" "$ICONSET_DIR/icon_256x256@2x.png"
cp "$ICON_SRC/icon_512x512.png" "$ICONSET_DIR/icon_512x512.png"
cp "$ICON_SRC/icon_512x512@2x.png" "$ICONSET_DIR/icon_512x512@2x.png"
iconutil -c icns "$ICONSET_DIR" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns" 2>/dev/null || true
else
echo "WARNING: App icon assets not found at $ICON_SRC, skipping icon generation"
fi

# Copy status bar icon
cp "$PROJECT_DIR/Resources/Assets.xcassets/StatusBarIcon.imageset/icon@1x.png" "$APP_BUNDLE/Contents/Resources/"
cp "$PROJECT_DIR/Resources/Assets.xcassets/StatusBarIcon.imageset/icon@2x.png" "$APP_BUNDLE/Contents/Resources/"
# Copy status bar icon (if available)
STATUSBAR_SRC="$PROJECT_DIR/Resources/Assets.xcassets/StatusBarIcon.imageset"
if [ -d "$STATUSBAR_SRC" ]; then
cp "$STATUSBAR_SRC/icon@1x.png" "$APP_BUNDLE/Contents/Resources/"
cp "$STATUSBAR_SRC/icon@2x.png" "$APP_BUNDLE/Contents/Resources/"
else
echo "WARNING: Status bar icon assets not found, skipping"
fi

echo "==> Signing (ad-hoc)..."
codesign --force --sign - "$APP_BUNDLE"
Expand Down
56 changes: 56 additions & 0 deletions Scripts/probe_endpoints.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash
# UniFi API Endpoint Probe
# Usage: bash probe_endpoints.sh <controller_url> <api_key>
# Example: bash probe_endpoints.sh https://192.168.2.1 f81179df...

CONTROLLER="$1"
API_KEY="$2"

if [ -z "$CONTROLLER" ] || [ -z "$API_KEY" ]; then
echo "Usage: bash probe_endpoints.sh <controller_url> <api_key>"
exit 1
fi

# Strip trailing slash
CONTROLLER="${CONTROLLER%/}"

HEADER="X-API-KEY: $API_KEY"
NOW_MS=$(($(date +%s) * 1000))
HOUR_AGO_MS=$((NOW_MS - 3600000))

endpoints=(
"GET|/proxy/network/api/s/default/rest/alarm|alarms_rest"
"GET|/proxy/network/api/s/default/list/alarm|alarms_list"
"GET|/proxy/network/api/s/default/stat/ips/event|ips_events"
"GET|/proxy/network/api/s/default/rest/dynamicdns|ddns"
"GET|/proxy/network/api/s/default/rest/portforward|portforwards"
"GET|/proxy/network/api/s/default/stat/rogueap|rogueaps"
)

echo "=== UniFi API Endpoint Probe ==="
echo "Controller: $CONTROLLER"
echo "Time: $(date -u)"
echo ""

for entry in "${endpoints[@]}"; do
IFS='|' read -r method path label <<< "$entry"
url="${CONTROLLER}${path}"
echo "--- $label ---"
echo "$method $path"

if [ "$method" = "POST" ]; then
response=$(curl -sk -w "\n__HTTP_CODE__%{http_code}" \
-X POST -H "$HEADER" -H "Content-Type: application/json" \
-d '{"type":"by_cat"}' "$url" 2>/dev/null)
else
response=$(curl -sk -w "\n__HTTP_CODE__%{http_code}" "$url" -H "$HEADER" 2>/dev/null)
fi

http_code=$(echo "$response" | grep "__HTTP_CODE__" | sed 's/__HTTP_CODE__//')
body=$(echo "$response" | grep -v "__HTTP_CODE__")

echo "HTTP $http_code"
echo "$body" | head -c 800
echo ""
echo ""
done
4 changes: 4 additions & 0 deletions Sources/UniFiBar/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}

func applicationWillTerminate(_ notification: Notification) {
controller.tearDown()
}
}
2 changes: 2 additions & 0 deletions Sources/UniFiBar/App/UniFiBarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ struct UniFiBarApp: App {
SetupView(controller: controller)
}
.windowResizability(.contentSize)
.defaultSize(width: 380, height: 440)

Window("Preferences", id: "preferences") {
PreferencesView(controller: controller)
}
.windowResizability(.contentSize)
.defaultSize(width: 480, height: 700)
}
}
33 changes: 29 additions & 4 deletions Sources/UniFiBar/Models/DeviceDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct WANHealth: Sendable {
let drops: Int?
let rxBytesRate: Double?
let txBytesRate: Double?
let speedTest: SpeedTestResult?
}

struct WANHealthResponse: Decodable, Sendable {
Expand All @@ -57,6 +58,13 @@ struct WANHealthResponse: Decodable, Sendable {
let rxBytesR: Double?
let txBytesR: Double?

// Speed test fields
let speedtestLastrun: Int?
let speedtestPing: Int?
let speedtestStatus: String?
let xputDown: Double?
let xputUp: Double?

enum CodingKeys: String, CodingKey {
case subsystem, status
case ispName = "isp_name"
Expand All @@ -65,6 +73,11 @@ struct WANHealthResponse: Decodable, Sendable {
case latency, drops
case rxBytesR = "rx_bytes-r"
case txBytesR = "tx_bytes-r"
case speedtestLastrun = "speedtest_lastrun"
case speedtestPing = "speedtest_ping"
case speedtestStatus = "speedtest_status"
case xputDown = "xput_down"
case xputUp = "xput_up"
}
}

Expand All @@ -87,6 +100,19 @@ struct WANHealthResponse: Decodable, Sendable {
let www = data.first(where: { $0.subsystem == "www" })
guard wan != nil || www != nil else { return nil }

let speedTest: SpeedTestResult?
if let lastrun = wan?.speedtestLastrun, lastrun > 0 {
speedTest = SpeedTestResult(
downloadMbps: wan?.xputDown,
uploadMbps: wan?.xputUp,
pingMs: wan?.speedtestPing,
lastRun: Date(timeIntervalSince1970: TimeInterval(lastrun)),
status: wan?.speedtestStatus
)
} else {
speedTest = nil
}

return WANHealth(
ispName: wan?.ispName,
wanIP: wan?.wanIP,
Expand All @@ -95,7 +121,8 @@ struct WANHealthResponse: Decodable, Sendable {
availability: wan?.uptimeStats?.WAN?.availability,
drops: www?.drops,
rxBytesRate: wan?.rxBytesR,
txBytesRate: wan?.txBytesR
txBytesRate: wan?.txBytesR,
speedTest: speedTest
)
}
}
Expand Down Expand Up @@ -154,7 +181,6 @@ struct APStats: Sendable {
let uptimeSec: Int?
let cpuUtilizationPct: Double?
let memoryUtilizationPct: Double?
let txRetriesPct: Double?
}

struct APStatsResponse: Decodable, Sendable {
Expand All @@ -166,8 +192,7 @@ struct APStatsResponse: Decodable, Sendable {
APStats(
uptimeSec: uptimeSec,
cpuUtilizationPct: cpuUtilizationPct,
memoryUtilizationPct: memoryUtilizationPct,
txRetriesPct: nil
memoryUtilizationPct: memoryUtilizationPct
)
}
}
Expand Down
Loading