Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 14 additions & 5 deletions .github/workflows/pr-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: PR Agent

on:
pull_request:
types: [opened, ready_for_review, reopened]
types: [opened, ready_for_review, reopened, synchronize]
issue_comment:
types: [created]

Expand All @@ -17,19 +17,28 @@ jobs:
(github.event_name == 'pull_request' && (
github.event.action == 'opened' ||
github.event.action == 'ready_for_review' ||
github.event.action == 'reopened'
github.event.action == 'reopened' ||
github.event.action == 'synchronize'
)) ||
(github.event_name == 'issue_comment' &&
startsWith(github.event.comment.body, '/pr-agent'))
github.event.sender.type != 'Bot' && (
startsWith(github.event.comment.body, '/review') ||
startsWith(github.event.comment.body, '/improve') ||
startsWith(github.event.comment.body, '/describe') ||
startsWith(github.event.comment.body, '/update_changelog') ||
startsWith(github.event.comment.body, '/add_docs') ||
startsWith(github.event.comment.body, '/pr-agent')
))
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
steps:
- name: PR Agent
uses: qodo-ai/pr-agent@d82f7d3e696cd00822694aaa3096265d3889f3f1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
OPENAI_API_BASE: ${{ secrets.OPENAI_API_BASE }}
CONFIG__MODEL: "glm-5.1:cloud"
CONFIG__MODEL: "openai/glm-5.1:cloud"
GITHUB_ACTION_CONFIG__PR_ACTIONS: '["opened","reopened","ready_for_review","synchronize"]'
PR_REVIEWER__ENABLE_AUTO_REVIEW: "true"
PR_CODE_SUGGESTIONS__ENABLE_AUTO_CODE_SUGGESTIONS: "true"
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ DerivedData/
.swiftpm/
/tmp/
unifbar-debug.log
.env

# Debug scripts with credential access
Scripts/api_debug.swift
Expand Down
9 changes: 8 additions & 1 deletion .pr_agent.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
[config]
model = "glm-5.1:cloud"
model = "openai/glm-5.1:cloud"
fallback_models = []
custom_model_max_tokens = 32768
custom_reasoning_model = true

[github_app]
handle_push_trigger = true
push_commands = ["/review"]

[pr_reviewer]
enable_auto_review = true
extra_instructions = """\
Focus on security, correctness, and Swift 6 strict concurrency.
Flag any use of unsafe concurrency patterns, unguarded mutable state, or missing Sendable conformance.
Expand All @@ -14,4 +20,5 @@ Check for force unwraps, unbounded arrays, and credential leaks in logs.
extra_instructions = "Include a security impact assessment in the description."

[pr_code_suggestions]
enable_auto_code_suggestions = true
max_suggestions = 4
5 changes: 5 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ let package = Package(
resources: [
.copy("../../Resources/Assets.xcassets")
]
),
.testTarget(
name: "UniFiBarTests",
dependencies: ["UniFiBar"],
path: "Tests/UniFiBarTests"
)
]
)
55 changes: 35 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 @@ -60,6 +64,8 @@ cat > "$APP_BUNDLE/Contents/Info.plist" << PLIST
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>UniFiBar needs access to your local network to connect to your UniFi controller.</string>
</dict>
</plist>
PLIST
Expand All @@ -69,26 +75,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
55 changes: 35 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 @@ -56,30 +60,41 @@ cat > "$APP_BUNDLE/Contents/Info.plist" << PLIST
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>UniFiBar needs access to your local network to connect to your UniFi controller.</string>
</dict>
</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
53 changes: 53 additions & 0 deletions Scripts/probe_endpoints.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/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/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()
}
}
1 change: 1 addition & 0 deletions Sources/UniFiBar/App/UniFiBarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct UniFiBarApp: App {
SetupView(controller: controller)
}
.windowResizability(.contentSize)
.defaultSize(width: 380, height: 440)

Window("Preferences", id: "preferences") {
PreferencesView(controller: controller)
Expand Down
32 changes: 32 additions & 0 deletions Sources/UniFiBar/Models/ClientDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,38 @@ struct V2ClientDTO: Decodable, Sendable {
let ccq: Int?
let gwMac: String?

/// Minimal init for wired connections not found in UniFi client list.
/// Only the IP is known; all other fields are nil.
init(ip: String, mac: String? = nil, hostname: String? = nil, displayName: String? = nil, signal: Int? = nil, rssi: Int? = nil, noise: Int? = nil, satisfaction: Int? = nil, wifiExperienceScore: Int? = nil, wifiExperienceAverage: Int? = nil, wifiTxRetriesPercentage: Double? = nil, channel: Int? = nil, channelWidth: Int? = nil, radioProto: String? = nil, radio: String? = nil, essid: String? = nil, apMac: String? = nil, lastUplinkName: String? = nil, rxRate: Int? = nil, txRate: Int? = nil, rxBytes: Int? = nil, txBytes: Int? = nil, uptime: Int? = nil, mimo: String? = nil, roamCount: Int? = nil, ccq: Int? = nil, gwMac: String? = nil) {
self.mac = mac
self.ip = ip
self.hostname = hostname
self.displayName = displayName
self.signal = signal
self.rssi = rssi
self.noise = noise
self.satisfaction = satisfaction
self.wifiExperienceScore = wifiExperienceScore
self.wifiExperienceAverage = wifiExperienceAverage
self.wifiTxRetriesPercentage = wifiTxRetriesPercentage
self.channel = channel
self.channelWidth = channelWidth
self.radioProto = radioProto
self.radio = radio
self.essid = essid
self.apMac = apMac
self.lastUplinkName = lastUplinkName
self.rxRate = rxRate
self.txRate = txRate
self.rxBytes = rxBytes
self.txBytes = txBytes
self.uptime = uptime
self.mimo = mimo
self.roamCount = roamCount
self.ccq = ccq
self.gwMac = gwMac
}

enum CodingKeys: String, CodingKey {
case mac, ip, hostname, signal, rssi, noise, satisfaction, channel, radio, essid, uptime, ccq
case displayName = "display_name"
Expand Down
Loading