v2.0.9 — limit notifications at 80%/95% and on reset
This commit is contained in:
@@ -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])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
[](https://www.apple.com/macos/)
|
[](https://www.apple.com/macos/)
|
||||||
[](https://swift.org)
|
[](https://swift.org)
|
||||||
[](https://github.com/superdooper86/claudechecker/releases)
|
[](https://github.com/superdooper86/claudechecker/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+3
-3
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user