feat: track limit budgets in a usage ledger and surface them in a Usage section

- RuleUsage + UsageLedger: per-rule, per-day minutes/opens in app-group
  defaults (monotonic minutes, incrementing opens, midnight rollover by
  day-keying); MockUsageLedger for tests and seeded UI scenarios
- usage-aware status: a limit rule whose daily budget is spent on an
  enabled day is active (blocking) until next midnight; Hard Mode gating
  and app-list locking honor it; soft unblock pauses until midnight
- RuleEnforcer shields spent limit rules (always Block mode) while the
  app runs
- new Usage section under Blocked Apps: '18m of 45m used today · 27m
  left', '2 of 5 opens today · 3 opens left', 'Blocked until tomorrow'
- new 'limits' seed scenario + UI tests
This commit is contained in:
2026-06-12 20:22:48 -04:00
parent cb9b2e8950
commit 8e3b8b70f5
14 changed files with 640 additions and 62 deletions

View File

@@ -15,6 +15,7 @@ struct AppListPickerSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@Environment(RuleEnforcer.self) private var enforcer
@Query(sort: \AppList.createdAt) private var lists: [AppList]
@Query private var rules: [BlockingRule]
@@ -22,10 +23,11 @@ struct AppListPickerSheet: View {
@State private var creatingList = false
@State private var deletionBlocked = false
/// While any hard-mode rule is actively blocking, lists are read-only
/// editing one would be a back door out of the hard block.
/// While any hard-mode rule is actively blocking (by the clock or a spent
/// limit budget), lists are read-only editing one would be a back door
/// out of the hard block.
private var listsLocked: Bool {
!RulePolicy.canEditAppLists(rules: rules)
!RulePolicy.canEditAppLists(rules: rules, usageFor: { enforcer.usage(for: $0) })
}
var body: some View {