fix: refresh triggers + L1 observability + banner diagnostics (build 28) - #17
Merged
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three logger categories were silently filtered out of "Copy Recent Logs": - fg-sub (ForegroundRelaySubscription) — every L1 lifecycle log invisible - banner (PendingApprovalBanner) — every banner schedule attempt invisible - nc-sweep (NotificationCenterSweep) — every NC sweep invisible This made it appear that L1 was silently not running and that pending- approval banners were failing without trace. Adding them to allCategories unblocks diagnosis on the next test session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleForegroundSigningRequest only posted .signingCompleted when handledCount > 0. When NSE pre-deduped an event (handledCount=0 because LightSigner returns "skipped-duplicate"), the main app received no refresh signal even though NSE had already written a pending request to cross-process storage. Symptom: user gets a protected-event sign request while Clave is foregrounded, the pending row doesn't appear in HomeView until they navigate away and back (which fires MainTabView's scenePhase observer). Now post both .pendingRequestsUpdated (AppState observes → refreshes pending list) and .signingCompleted (HomeView/ActivityView observe → refresh derived counters) unconditionally on every foreground push. refreshPendingRequests is a single disk read; cost is negligible. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After L1 catches and processes a sign request, requestCount and signedTodayCount get persisted via SharedStorage.touchClient and logActivity (called inside LightSigner.handleRequest), but HomeView and ActivityView never re-read those counters. They observe .signingCompleted. Posting unconditionally (regardless of result.status) covers signed, skipped-duplicate, pending, and error — all of which represent storage state that views may want to display. The pending-list refresh path remains covered by SharedStorage.queuePendingRequest posting .pendingRequestsUpdated; this fix only addresses the counter-display gap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two start() guards previously set state=.error without logging, so the OSLog buffer had no record of why L1 failed to start (no-signer-key vs no-relays vs not-idle). Both paths now log before returning. Add a setState helper that logs every state transition to [fg-sub] so a single grep gives a complete lifecycle timeline. Replaces scattered direct state= assignments in start/stop/runDispatcher/runRelayLoop. Expand the start log to include relay URLs (privacy=.public — these are public WSS endpoints) so a single bad URL poisoning the set is visible in logs. Previously only count was logged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new @observable properties surface the dispatcher's runtime state to the upcoming L1DiagnosticsView: - sessionStartedAt: nil when L1 isn't running, set on first transition to .listening within a dispatcher run, cleared on dispatcher exit. Spans transient .reconnecting bounces — answers "how long has L1 been alive?" rather than "how long since the last frame?". Cellular connections bounce routinely; clearing on every reconnect would make the view flicker uselessly. - currentRelays: relay URLs the current run is subscribed to. Empty when L1 isn't running. Lets the diagnostics view render the actual relay set so users can spot "L1 connected to wrong relays" without parsing log output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NavigationLink in Settings → Developer (gated behind 7-tap unlock) opens a new L1DiagnosticsView that surfaces ForegroundRelaySubscription's runtime state to a tester without requiring Xcode Console: - State (color-coded), status message, session age (live timer) - Counters: received / processed / failed - Latency p50 + p95 from existing recentLatenciesMs ring buffer - Last error (red, when non-nil) - Currently subscribed relays (the actual URLs L1 is hitting) - iOS notification permission status — directly addresses the "banner doesn't appear" surface. If alert/sound/badge are disabled or authorization is denied, the section turns red. "Open iOS Notification Settings" button uses openNotificationSettingsURLString (iOS 16+). - Restart L1 + Reset counters buttons for fast manual verification. Access pattern: @State on ForegroundRelaySubscription.shared. @observable reference-tracking handles re-renders without environment plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three branches in userNotificationCenter willPresent now log their decision: local pending-approval banner shown, APNs push with non-empty title shown, APNs push with empty title suppressed. Combined with the LogExporter fix surfacing [Banner] schedule logs and [fg-sub] processEvent logs, this gives a complete user-visible-banner trace. A failed banner can now be diagnosed end-to-end: [fg-sub] processEvent eid=... [Banner] Scheduled pending-approval banner client=... kind=... [App] willPresent: local banner id=pending-approval-... — show If any link is missing, that's the actionable break. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rift ForegroundRelaySubscriptionTests: - sessionStartedAt nil at idle / on bail / not cleared by resetCounters - currentRelays empty at idle - State.rawValue stability for log greps (renames break the test, not user log analysis silently) LogExporterFormattingTests: - Regression guard: allCategories must include every shipped Logger category declaration. Catches the class of bug where a new feature ships its own Logger but forgets to update the export filter, leaving the user blind to that subsystem's activity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- import Combine for Timer.publish().autoconnect() - explicit Color type on the authorization-status ternary so the ShapeStyle inference picks the right overload Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users who imported their key in an older build had an empty signerPubkeyHexKey in app-group UserDefaults — only importKey() and generateKey() ever wrote this cache, while loadState() (the read-existing-key path) populated only the in-memory AppState property. L1's start() reads from UserDefaults (not AppState directly, since L1 runs on its own MainActor and is owned by the singleton, not AppState), so for affected users L1 saw "no signer key configured" and bailed silently. NSE was unaffected — it loads the nsec from Keychain on each wake. Symptom: pending events flowed through APNs+NSE, but L1 never ran, and the foreground push handler was the only main-process refresh path (which Bug 1 in this same PR also fixed). loadState now writes the derived pubkey hex back to UserDefaults if the cached value is stale or empty. One-time backfill per install; the cache is correct forever after. pbxproj 28 → 29 since build 28 is already in TestFlight internal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Build 28 bundles three small refresh-trigger fixes with a much larger
diagnostic unlock: three logger categories that have been silently
filtered out of "Copy Recent Logs" since PR #11 / PR #13 are now
included, and a new dev-menu L1 Diagnostics view surfaces
ForegroundRelaySubscription's runtime state alongside iOS notificationpermission status.
The previous-session diagnosis "L1 is silently not running" was almost
certainly a visibility artifact: the
fg-sub,banner, andnc-sweepcategories were missing from
LogExporter.allCategories, so every L1log line, every banner schedule attempt, and every NC sweep was being
filtered out of the user's exported logs. Adding them is a one-line fix
that makes the next test session immediately diagnostic.
The previous-session prompt also claimed
AppStateobserved.signingCompleted. Verified by grep: it does not.AppStateobserves.pendingRequestsUpdated. The pending-list refresh fix posts BOTHsignals so each subscriber refreshes the data it cares about.
Refresh-trigger fixes
Foreground push handler (
ClaveApp.swift handleForegroundSigningRequest):posts
.pendingRequestsUpdated+.signingCompletedunconditionallyinstead of gating on
handledCount > 0. Fixes the user-visible bugwhere the protected-event approve row didn't appear in HomeView while
Clave was foregrounded — you had to navigate away and back to see it.
L1
processEvent(Shared/ForegroundRelaySubscription.swift):posts
.signingCompletedafter every event regardless of resultstatus. Fixes counter staleness (signedToday, per-client requestCount
badges) when L1 catches a request before NSE.
L1 diagnostic logging
start()silent-return paths (no signer key, no relays) now logbefore returning. Previously set
state=.errorsilently; OSLog hadno record of why L1 failed to start.
setState(_:message:)helper — single chokepoint that logs everytransition (
idle → starting → listening → reconnecting → ...).Replaces scattered direct
state =assignments. Lets a tester readthe full L1 lifecycle from one grep.
start()log expanded to include relay URLs (privacy=.public —public WSS endpoints) so a single bad URL poisoning the set is
visible.
L1 observability surface
@Observableproperties:sessionStartedAt(set on first.listeningof a dispatcher run, cleared on.idle, spans.reconnectingbounces),currentRelays(URLs the run is subscribedto).
Clave/Views/Settings/L1DiagnosticsView.swift— Form-based devmenu sub-view (NavigationLink from Settings → Developer, after the
7-tap unlock). Surfaces:
recentLatenciesMsring"banner doesn't appear" surface. When alert/sound/badge are
disabled or authorization is denied, the section turns red.
"Open iOS Notification Settings" deep-links via
openNotificationSettingsURLString(iOS 16+).Banner diagnostic logging
userNotificationCenter willPresentlogs its decision in all threebranches (local-banner show, APNs-non-empty-title show, APNs-empty
suppress). Combined with the LogExporter fix, the next failed
banner is end-to-end traceable:
Tests
ForegroundRelaySubscriptionTests(sessionStartedAt /currentRelays semantics, State.rawValue stability for log greps)
LogExporterFormattingTestsregression guard (allCategoriesmust include every shipped Logger category — catches the bug class
this PR fixes)
xcodebuild testreturnsTEST SUCCEEDEDon iPhone 17 Pro Max iOS 26.4
Test plan
Phase A — Local Debug build (no TF needed)
.listening, session age ticking,relays section shows expected URLs (
relay.powr.buildminimum)toggle off in iOS Settings → returns red on next 5s tick
[fg-sub],[Banner],[NCSweep]linesstat increments without manual refresh (Bug 2 fix)
Phase B — Internal TestFlight (build 28 only)
client via APNs → pending row appears in HomeView immediately
(no swipe / navigate-away required) — Bug 1 fix, the user-visible
issue from build 27 testing
notification → foregrounds Clave → pending row appears (PR fix: pending-approval refresh + banner notifications + UI bundle #13
scenePhase observer; confirm no regression)
🤖 Generated with Claude Code