2626from .connection .admission_transaction import (
2727 CollectorAdmissionTransaction ,
2828)
29+ from .connection .callback_identity import OnboardingWireProbeIntent
2930from .connection .connection_policy import (
3031 collector_identity_binding_required ,
3132 resolve_connection_strategy ,
5556 CONF_DETECTED_SERIAL ,
5657 CONF_DETECTION_CONFIDENCE ,
5758 CONF_DISCOVERY_TARGET ,
59+ CONF_DRIVER_HINT ,
5860 CONF_DRIVER_DETECTION_STRATEGY ,
5961 CONF_POLL_INTERVAL ,
6062 CONF_POLL_MODE ,
@@ -133,6 +135,8 @@ async def async_step_reconfigure(
133135
134136 self ._repair_entry_id = entry .entry_id
135137 await self ._async_ensure_network_defaults ()
138+ if not self ._manual_config :
139+ self ._manual_defaults = {** entry .data , ** entry .options }
136140
137141 # A PN-less entry has no prior identity to match -> bind ANY freshly
138142 # triggered strong session. An entry that still carries a (short) PN must
@@ -152,24 +156,60 @@ async def async_step_reconfigure(
152156 )
153157 if not errors :
154158 self ._manual_config = dict (flat_input )
155- # Repair is an active callback attempt too: same shared lifecycle
156- # (fresh baseline + ledger generation + probe + matcher + claim),
157- # so a repeated repair can never reuse a previous attempt's proof.
158- verification_error = await self ._async_run_manual_callback_attempt (
159- flat_input
160- )
161- if verification_error :
162- errors ["base" ] = verification_error
163- else :
164- applied = self ._async_apply_reconfigure (entry )
165- if applied is not None :
166- return applied
167- # No durable strong PN was bound: refuse to "repair" into
168- # another doomed PN-less entry; re-prompt.
169- errors ["base" ] = "callback_identity_unverified"
159+ return await self ._async_reconfigure_retry ()
170160
171161 return self ._async_show_reconfigure_form (user_input , errors )
172162
163+ async def async_step_reconfigure_confirm (
164+ self , user_input : dict [str , Any ] | None = None ,
165+ ) -> ConfigFlowResult :
166+ """Serve repair results with the shared explicit identity actions."""
167+
168+ del user_input
169+ if not self ._repair_entry_id :
170+ return await self .async_step_reconfigure ()
171+ return await self .async_step_manual_confirm ()
172+
173+ async def _async_reconfigure_retry (
174+ self , protocol : str | None = None ,
175+ ) -> ConfigFlowResult :
176+ """Retry identity repair; never enter new-entry save or reboot recovery."""
177+
178+ entry = self ._reconfigure_target_entry ()
179+ if entry is None or not collector_identity_binding_required (
180+ entry .data , entry .options ,
181+ ):
182+ self ._replace_manual_callback_continuation ()
183+ return self .async_abort (reason = (
184+ "reconfigure_entry_missing" if entry is None else "reconfigure_not_required"
185+ ))
186+ if not self ._manual_config :
187+ return await self .async_step_reconfigure ()
188+ intent = None
189+ if protocol is not None :
190+ offer = self ._callback_continuation .silent_bootstrap_offer
191+ if offer is None :
192+ return await self .async_step_manual_confirm ()
193+ intent = OnboardingWireProbeIntent .for_offer (offer , protocol = protocol )
194+ error = await self ._async_run_manual_callback_attempt (
195+ self ._manual_config , bootstrap_probe = intent ,
196+ )
197+ if not error :
198+ # Re-check after the await: removal or another repair may have won.
199+ current = self ._reconfigure_target_entry ()
200+ if current is None or not collector_identity_binding_required (
201+ current .data , current .options ,
202+ ):
203+ self ._replace_manual_callback_continuation ()
204+ return self .async_abort (reason = (
205+ "reconfigure_entry_missing" if current is None else "reconfigure_not_required"
206+ ))
207+ applied = self ._async_apply_reconfigure (current )
208+ if applied is not None :
209+ return applied
210+ error = "callback_identity_unverified"
211+ return await self ._async_route_after_manual_callback_failure (error )
212+
173213 def _async_show_reconfigure_form (
174214 self ,
175215 user_input : dict [str , Any ] | None ,
@@ -236,8 +276,22 @@ def _async_apply_reconfigure(self, entry: ConfigEntry) -> ConfigFlowResult | Non
236276 return self .async_abort (reason = "already_configured" )
237277
238278 new_data = dict (entry .data )
279+ new_options = dict (entry .options )
280+ # Retain the verified route, including an explicitly corrected listener
281+ # port. An old option override must not silently restore the broken route
282+ # after reload. Identity repair does not change driver/control policy.
283+ for key , value in build_manual_entry_settings (
284+ self ._current_connection_type (), self ._manual_config ,
285+ ).items ():
286+ if key == CONF_DRIVER_HINT :
287+ continue
288+ new_data [key ] = value
289+ if key in new_options :
290+ new_options [key ] = value
239291 new_data [CONF_COLLECTOR_PN ] = verified_full_pn
240292 new_data [CONF_COLLECTOR_IP ] = collector_ip
293+ if CONF_COLLECTOR_IP in new_options :
294+ new_options [CONF_COLLECTOR_IP ] = collector_ip
241295 # Identity repair re-binds the durable PN; it does NOT re-decide how the
242296 # collector connects. The entry's canonical strategy (the user's choice)
243297 # is preserved untouched. Only an entry from before the canonical axis
@@ -275,6 +329,7 @@ def _async_apply_reconfigure(self, entry: ConfigEntry) -> ConfigFlowResult | Non
275329 entry ,
276330 unique_id = f"collector:{ verified_full_pn } " ,
277331 data = new_data ,
332+ options = new_options ,
278333 ),
279334 recovery = self ._callback_continuation .terminal_input ,
280335 )
0 commit comments