v2.0.9 — limit notifications at 80%/95% and on reset

This commit is contained in:
superdooper86
2026-05-07 15:40:13 +02:00
parent 2b48114996
commit b95102cb1b
6 changed files with 72 additions and 7 deletions
+12 -1
View File
@@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import WebKit import WebKit
import Combine import Combine
import UserNotifications
@main @main
struct ClaudeCheckerApp: App { struct ClaudeCheckerApp: App {
@@ -11,7 +12,7 @@ struct ClaudeCheckerApp: App {
} }
@MainActor @MainActor
class AppDelegate: NSObject, NSApplicationDelegate { class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {
var statusItem: NSStatusItem? var statusItem: NSStatusItem?
var popover: NSPopover? var popover: NSPopover?
var usageViewModel = UsageViewModel() var usageViewModel = UsageViewModel()
@@ -23,6 +24,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory) NSApp.setActivationPolicy(.accessory)
let center = UNUserNotificationCenter.current()
center.delegate = self
center.requestAuthorization(options: [.alert, .sound]) { _, _ in }
// Hidden WKWebView to prime the shared cookie store // Hidden WKWebView to prime the shared cookie store
let config = WKWebViewConfiguration() let config = WKWebViewConfiguration()
config.websiteDataStore = WKWebsiteDataStore.default() config.websiteDataStore = WKWebsiteDataStore.default()
@@ -205,4 +210,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
refreshTimer?.invalidate() refreshTimer?.invalidate()
} }
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler handler: @escaping (UNNotificationPresentationOptions) -> Void) {
handler([.banner, .sound])
}
} }
+2 -2
View File
@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.0.8</string> <string>2.0.9</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>15</string> <string>16</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>13.0</string> <string>13.0</string>
<key>LSUIElement</key> <key>LSUIElement</key>
+6
View File
@@ -31,6 +31,12 @@ enum WindowType: String {
case fiveHour = "5h" case fiveHour = "5h"
case sevenDay = "7d" case sevenDay = "7d"
var label: String { rawValue } var label: String { rawValue }
var displayName: String {
switch self {
case .fiveHour: return "5-hour"
case .sevenDay: return "7-day"
}
}
} }
enum ProjectedHit { enum ProjectedHit {
+48
View File
@@ -1,5 +1,6 @@
import SwiftUI import SwiftUI
import WebKit import WebKit
import UserNotifications
@MainActor @MainActor
class UsageViewModel: ObservableObject { class UsageViewModel: ObservableObject {
@@ -29,6 +30,8 @@ class UsageViewModel: ObservableObject {
private var burnHistoryStore: [String: [Double]] = [:] private var burnHistoryStore: [String: [Double]] = [:]
private let maxHistorySamples = 24 private let maxHistorySamples = 24
private var cachedOrgId: String? private var cachedOrgId: String?
private var previousPercents: [String: Double] = [:]
private var firedThresholds: [String: Set<Int>] = [:]
init() { init() {
cachedOrgId = UserDefaults.standard.string(forKey: "claude_org_id") ?? Self.orgId cachedOrgId = UserDefaults.standard.string(forKey: "claude_org_id") ?? Self.orgId
@@ -84,6 +87,7 @@ class UsageViewModel: ObservableObject {
burnHistoryStore[key] = history burnHistoryStore[key] = history
if history.count > 1 { limits[i].burnHistory = history } if history.count > 1 { limits[i].burnHistory = history }
} }
checkLimitNotifications(for: limits)
} catch AppError.notAuthenticated { } catch AppError.notAuthenticated {
isNotAuthenticated = true isNotAuthenticated = true
errorMessage = "Not signed in" errorMessage = "Not signed in"
@@ -234,6 +238,50 @@ class UsageViewModel: ObservableObject {
return result return result
} }
// MARK: - Limit notifications
private func checkLimitNotifications(for limits: [AgentLimit]) {
let center = UNUserNotificationCenter.current()
let thresholds = [80, 95]
for limit in limits {
let key = limit.window.rawValue
let curr = limit.usedPercent
let prev = previousPercents[key]
var fired = firedThresholds[key] ?? []
// Threshold warnings (80% and 95%)
for t in thresholds {
let threshold = Double(t)
guard curr >= threshold, prev ?? threshold < threshold, !fired.contains(t) else { continue }
fired.insert(t)
let content = UNMutableNotificationContent()
content.title = "Claude \(limit.window.displayName) limit"
content.body = t == 95
? "\(Int(curr.rounded()))% used — almost at your limit"
: "\(Int(curr.rounded()))% used — approaching your limit"
content.sound = .default
center.add(UNNotificationRequest(identifier: "cc_limit_\(key)_\(t)", content: content, trigger: nil))
}
// Reset detection: was high, now low
if let prev, prev > 50, curr < 20 {
fired.removeAll()
let content = UNMutableNotificationContent()
content.title = "Claude \(limit.window.displayName) limit reset"
content.body = "Your \(limit.window.displayName) quota has been reset"
content.sound = .default
center.add(UNNotificationRequest(
identifier: "cc_reset_\(key)_\(Int(Date().timeIntervalSince1970))",
content: content, trigger: nil
))
}
firedThresholds[key] = fired
previousPercents[key] = curr
}
}
private func loadPlaceholderData() { private func loadPlaceholderData() {
let now = Date() let now = Date()
limits = [ limits = [
+1 -1
View File
@@ -8,7 +8,7 @@
[![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/) [![macOS](https://img.shields.io/badge/macOS-13.0+-000000?style=flat&logo=apple&logoColor=white)](https://www.apple.com/macos/)
[![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org) [![Swift](https://img.shields.io/badge/Swift-5.9-F05138?style=flat&logo=swift&logoColor=white)](https://swift.org)
[![Version](https://img.shields.io/badge/version-2.0.8-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases) [![Version](https://img.shields.io/badge/version-2.0.9-orange?style=flat)](https://github.com/superdooper86/claudechecker/releases)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)
</div> </div>
+3 -3
View File
@@ -1,5 +1,5 @@
{ {
"version": "2.0.8", "version": "2.0.9",
"url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.8/ClaudeChecker.zip", "url": "https://github.com/superdooper86/claudechecker/releases/download/v2.0.9/ClaudeChecker.zip",
"notes": "Updated data sources list in settings." "notes": "Added limit notifications: warns at 80% and 95% usage, and notifies when a limit resets."
} }