fix: show a generic "App Blocked" shield instead of mis-attributing a rule

The shield titled itself with the first enabled rule whose selection
contained the app, regardless of whether that rule was the one actually
blocking. When several rules covered the same app (e.g. a dormant
schedule + the active limit) it named the wrong rule.

Drop the rule-name lookup entirely: every shield now carries a generic
"App Blocked" title. Open-limit shields keep their functional detail
(running count + "Open (N left)" button) beneath that title. The
text-only decision moves into a pure, unit-tested ShieldPresentation in
Shared/, and the now-dead ShieldLookup.snapshot(containingApplication:)
is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 15:32:26 -04:00
parent d92a8588bb
commit e5f571a63e
5 changed files with 127 additions and 52 deletions

View File

@@ -485,9 +485,14 @@ Xh", "Xh left") is **derived**, not stored.
`OpenSessionStore` reports a still-running granted open for it — so the
foreground loop establishes the turnstile for newly created rules and never
re-locks an app mid-session.
- **`OpenAppLockShieldConfig`** (ShieldConfiguration extension): open-limit
shields show "Opened X of N times today" with an "Open (Y left)" secondary
button; other shields show the blocking rule's name.
- **`OpenAppLockShieldConfig`** (ShieldConfiguration extension): every shield
carries the same generic **"App Blocked"** title — rule names are never shown,
since the rule a shield is attributed to cannot be determined reliably when
several rules cover the same app. Open-limit shields keep their functional
detail under that title ("Opened X of N times today" with an "Open (Y left)"
secondary button while opens remain); all other shields just read "This app is
blocked by OpenAppLock." The text-only decision lives in the pure, unit-tested
`ShieldPresentation` (in `Shared/`).
- **`OpenAppLockShieldAction`** (ShieldAction extension): the Open press
spends one open in the ledger, lifts the rule's shield, records the session
expiry in `OpenSessionStore`, and starts the ~15-minute one-shot session