diff --git a/macos-bar/Scripts/package_app.sh b/macos-bar/Scripts/package_app.sh index d03e701b..e23927d1 100755 --- a/macos-bar/Scripts/package_app.sh +++ b/macos-bar/Scripts/package_app.sh @@ -54,10 +54,10 @@ case "$SIGNING" in ;; esac -ZIP="$DIST/CCS-Bar.zip" +ZIP="$DIST/CCS-Bar.app.zip" echo "[i] Zipping -> $ZIP" rm -f "$ZIP" -( cd "$DIST" && ditto -c -k --keepParent "$APP_NAME.app" "CCS-Bar.zip" ) +( cd "$DIST" && ditto -c -k --keepParent "$APP_NAME.app" "CCS-Bar.app.zip" ) echo "[OK] Packaged: $APP" echo "[OK] Asset: $ZIP" diff --git a/macos-bar/Sources/CCSBarApp/BarViewModel.swift b/macos-bar/Sources/CCSBarApp/BarViewModel.swift index 26d69e0a..785fa2b4 100644 --- a/macos-bar/Sources/CCSBarApp/BarViewModel.swift +++ b/macos-bar/Sources/CCSBarApp/BarViewModel.swift @@ -83,7 +83,10 @@ final class BarViewModel: ObservableObject { perform { try await $0.solo(provider: row.provider, accountId: row.accountId) } } func setDefault(_ row: BarSummaryRow) { - perform { try await $0.setDefault(name: row.accountId) } + // The server's /api/accounts/default parses CLIProxy accounts as the + // composite "provider:accountId" key; row.id already has that shape. + // Sending the bare accountId fails parseCliproxyKey and 500s / no-ops. + perform { try await $0.setDefault(name: row.id) } } func tierLock(_ row: BarSummaryRow, tier: String?) { perform { try await $0.tierLock(provider: row.provider, tier: tier) } diff --git a/macos-bar/Sources/CCSBarCheck/main.swift b/macos-bar/Sources/CCSBarCheck/main.swift index 212b99fa..77e49a70 100644 --- a/macos-bar/Sources/CCSBarCheck/main.swift +++ b/macos-bar/Sources/CCSBarCheck/main.swift @@ -127,6 +127,16 @@ do { check(title.contains("$3.20"), "title shows total cost") } +// leadRow features the account CLOSEST TO EXHAUSTION (lowest remaining %), +// not the healthiest. quota_percentage is REMAINING quota. +let twoActive = [ + BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 90, health: "ok"), + BarSummaryRow(accountId: "b", provider: "agy", quotaPercentage: 30, health: "ok"), +] +let twoTitle = BarFormatting.statusTitle(rows: twoActive) +check(twoTitle.contains("30%"), "title features lowest-remaining (closest to exhaustion)") +check(!twoTitle.contains("90%"), "title does not feature the healthiest account") + // MARK: RefreshDebouncer (arms at decision time) var deb = RefreshDebouncer(interval: 15) diff --git a/macos-bar/Sources/CCSBarCore/BarFormatting.swift b/macos-bar/Sources/CCSBarCore/BarFormatting.swift index 76384ef2..75cda29c 100644 --- a/macos-bar/Sources/CCSBarCore/BarFormatting.swift +++ b/macos-bar/Sources/CCSBarCore/BarFormatting.swift @@ -30,12 +30,16 @@ public enum BarFormatting { return parts.joined(separator: " \u{00B7} ") } - /// The row to surface in the compact title: the one closest to exhaustion - /// (highest quota used). Rows without a known percentage sort last. + /// The row to surface in the compact title: the one closest to exhaustion. + /// `quota_percentage` is REMAINING quota (higher = more left), so the lead is + /// the LOWEST remaining percentage. Rows without a known percentage are not + /// chosen unless no row has one. static func leadRow(_ rows: [BarSummaryRow]) -> BarSummaryRow? { - rows.max { lhs, rhs in - (lhs.quotaPercentage ?? -1) < (rhs.quotaPercentage ?? -1) + let withPct = rows.filter { $0.quotaPercentage != nil } + if let lead = withPct.min(by: { ($0.quotaPercentage ?? 0) < ($1.quotaPercentage ?? 0) }) { + return lead } + return rows.first } }