Summary
Permit-join state was tracked in two places:
HomeView held local @State for permitJoinStartTime, permitJoinDuration, permitJoinTargetName, set only when the user started permit_join from the Home toolbar.
BridgeInfo.permitJoin / permitJoinEnd reflected the bridge's authoritative state.
When the user started permit_join from the Add Devices wizard (or from a different Z2M client), the toolbar button correctly lit up "active" because bridgeInfo.permitJoin was true — but tapping it opened the active sheet with empty start time, zero duration, and no target. The countdown didn't run; the via-router scope wasn't shown.
Compounding this, Z2M sends permit_join event payloads with the via-device, but bridge/info doesn't include it. Every periodic info refresh was wiping the captured target back to nil and recomputing permitJoinEnd (either jumping it forward when the timeout was static, or zeroing it when omitted) — so the countdown also flickered or disappeared in the wizard.
Fix (commit on dev)
BridgeInfo gains permitJoinTarget: String?, captured by the bridgeEvent handler from the permit_join payload's device field. A new copyUpdatingPermitJoin(enabled:timeout:target:) helper recomputes permitJoinEnd correctly when state actually changes.
bridge/info updates while permit_join stays on now preserve the existing permitJoinEnd, permitJoinTimeout, and permitJoinTarget instead of overwriting them. We only adopt the new ones once the bridge tells us permit_join itself flipped.
HomeView derives permitJoinStartTime, permitJoinTotalDuration, and permitJoinTargetName from bridgeInfo instead of local state. Both HomeView.sendPermitJoin and the wizard do an optimistic bridgeInfo write so the UI updates instantly without waiting for the bridge round-trip.
- Pairing wizard's "Network is open" row reads
permitJoinTarget and shows "Network is open via Kitchen Relay" when scoped, plain "Network is open" otherwise.
Acceptance criteria
Summary
Permit-join state was tracked in two places:
HomeViewheld local@StateforpermitJoinStartTime,permitJoinDuration,permitJoinTargetName, set only when the user started permit_join from the Home toolbar.BridgeInfo.permitJoin/permitJoinEndreflected the bridge's authoritative state.When the user started permit_join from the Add Devices wizard (or from a different Z2M client), the toolbar button correctly lit up "active" because
bridgeInfo.permitJoinwas true — but tapping it opened the active sheet with empty start time, zero duration, and no target. The countdown didn't run; the via-router scope wasn't shown.Compounding this, Z2M sends
permit_joinevent payloads with the via-device, butbridge/infodoesn't include it. Every periodic info refresh was wiping the captured target back to nil and recomputingpermitJoinEnd(either jumping it forward when the timeout was static, or zeroing it when omitted) — so the countdown also flickered or disappeared in the wizard.Fix (commit on
dev)BridgeInfogainspermitJoinTarget: String?, captured by thebridgeEventhandler from thepermit_joinpayload'sdevicefield. A newcopyUpdatingPermitJoin(enabled:timeout:target:)helper recomputespermitJoinEndcorrectly when state actually changes.bridge/infoupdates while permit_join stays on now preserve the existingpermitJoinEnd,permitJoinTimeout, andpermitJoinTargetinstead of overwriting them. We only adopt the new ones once the bridge tells us permit_join itself flipped.HomeViewderivespermitJoinStartTime,permitJoinTotalDuration, andpermitJoinTargetNamefrombridgeInfoinstead of local state. BothHomeView.sendPermitJoinand the wizard do an optimisticbridgeInfowrite so the UI updates instantly without waiting for the bridge round-trip.permitJoinTargetand shows "Network is open via Kitchen Relay" when scoped, plain "Network is open" otherwise.Acceptance criteria