From f19d9d8d4b06c1f8e4ed0927a2dd57db0fb5179d Mon Sep 17 00:00:00 2001 From: Marc Reynolds Date: Fri, 31 Jul 2026 15:13:38 -0600 Subject: [PATCH] feat(menubar): mark badge when a paired device is unreachable in combined scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under combined scope the badge falls back to the local figure whenever a paired device doesn't report (asleep / off-network), which read as a glitch. Append a dimmed "reachable/total" marker (e.g. "$95.94 /mo · 1/2") and a matching tooltip so the reduced total is legibly "peer unreachable" instead. The marker clears the instant every paired device reports again. Adds AppStore.menubarBadgeDeviceShortfall plus tests. --- mac/Sources/CodeBurnMenubar/AppStore.swift | 11 ++++ mac/Sources/CodeBurnMenubar/CodeBurnApp.swift | 19 ++++++- .../AppStoreRefreshRecoveryTests.swift | 57 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index 0ba383cf..3f09fdcf 100644 --- a/mac/Sources/CodeBurnMenubar/AppStore.swift +++ b/mac/Sources/CodeBurnMenubar/AppStore.swift @@ -285,6 +285,17 @@ final class AppStore { return cache[menubarCombinedKey]?.payload.combined?.combined } + /// `(reachable, total)` only when combined scope is active and fewer paired + /// devices reported than are paired — i.e. the badge total is degraded to + /// the reachable subset (a peer is asleep/off-network this cycle). The badge + /// shows this so a momentary drop to the local figure reads as "peer + /// unreachable", not a glitch. `nil` when every paired device reported (or + /// there is only one), and under local scope. + var menubarBadgeDeviceShortfall: (reachable: Int, total: Int)? { + guard let totals = menubarBadgeCombined, totals.reachableCount < totals.deviceCount else { return nil } + return (totals.reachableCount, totals.deviceCount) + } + /// Refresh the payloads the badge renders for `period`: always the local /// figure, plus the combined cross-device total when combined scope is /// active. Combined is best-effort — a slow or unreachable peer degrades to diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index f8b2958a..758dca11 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -1058,10 +1058,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM textAttrs[.foregroundColor] = NSColor.secondaryLabelColor } composed.append(NSAttributedString(string: valueText, attributes: textAttrs)) + + // Combined scope, but a paired device didn't report this cycle: append + // a dimmed "reachable/total" so the reduced total reads as "peer + // unreachable" rather than a glitch (mirrors the popover's device list). + if let shortfall = store.menubarBadgeDeviceShortfall { + let marker = " · \(shortfall.reachable)/\(shortfall.total)" + let markerAttrs: [NSAttributedString.Key: Any] = [ + .font: font, + .baselineOffset: -1.0, + .foregroundColor: NSColor.secondaryLabelColor, + ] + composed.append(NSAttributedString(string: marker, attributes: markerAttrs)) + } } button.attributedTitle = composed - button.toolTip = "CodeBurn \(menubarPeriod.menubarMetricLabel)" + if let shortfall = store.menubarBadgeDeviceShortfall { + button.toolTip = "CodeBurn \(menubarPeriod.menubarMetricLabel) · \(shortfall.reachable) of \(shortfall.total) devices reporting" + } else { + button.toolTip = "CodeBurn \(menubarPeriod.menubarMetricLabel)" + } persistBadgeStatusFile() } diff --git a/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift index d4e8b275..c021b456 100644 --- a/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift +++ b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift @@ -234,6 +234,63 @@ struct AppStoreRefreshRecoveryTests { #expect(store.menubarBadgeCombined?.cost == 75) } + @Test("badge reports a device shortfall when a paired peer is unreachable") + func menubarBadgeReportsDeviceShortfall() { + let store = AppStore() + store.suppressRefreshesForTesting() + let period = store.menubarPeriod + // Combined payload where only 1 of 2 paired devices reported this cycle + // (the peer is asleep/off-network), so the aggregate is degraded to local. + let degraded = CombinedUsage( + perDevice: [], + combined: CombinedUsageTotals( + cost: 30, + calls: 3, + sessions: 2, + inputTokens: 100, + outputTokens: 50, + cacheCreateTokens: 10, + cacheReadTokens: 20, + totalTokens: 180, + deviceCount: 2, + reachableCount: 1 + ) + ) + store.setCachedPayloadForTesting( + menubarPayload(cost: 30, combined: degraded), + scope: .combined, + period: period, + provider: .all, + fetchedAt: Date() + ) + store.selectedScope = .combined + + let shortfall = store.menubarBadgeDeviceShortfall + #expect(shortfall?.reachable == 1) + #expect(shortfall?.total == 2) + } + + @Test("badge reports no shortfall when every paired device reports") + func menubarBadgeNoShortfallWhenAllReachable() { + let store = AppStore() + store.suppressRefreshesForTesting() + let period = store.menubarPeriod + // combinedUsage() carries deviceCount == reachableCount == 1. + store.setCachedPayloadForTesting( + menubarPayload(cost: 30, combined: combinedUsage(cost: 30)), + scope: .combined, + period: period, + provider: .all, + fetchedAt: Date() + ) + store.selectedScope = .combined + #expect(store.menubarBadgeDeviceShortfall == nil) + + // Local scope never reports a shortfall. + store.selectedScope = .local + #expect(store.menubarBadgeDeviceShortfall == nil) + } + @Test("menubar badge falls back to local when no combined payload is cached") func menubarBadgeFallsBackWhenCombinedMissing() { let store = AppStore()