Enhance reconnect logic and error handling in UI and interface modules

- Implement automatic reconnection attempts in the main UI when disconnected.
- Add a non-blocking connection status dialog to inform users during reconnection.
- Improve error handling when sending messages and during interface reconnections.
- Update the reconnect_interface function to increase the number of attempts and refine connection checks.
This commit is contained in:
pdxlocations
2026-07-25 21:46:27 -07:00
parent 4ed3cbf370
commit 7f6fa49385
4 changed files with 83 additions and 13 deletions
+73 -6
View File
@@ -383,6 +383,41 @@ def main_ui(stdscr: curses.window) -> None:
handle_resize(stdscr, True)
while True:
interface = interface_state.interface
if (
hasattr(interface, "stream")
and interface.stream is None
and not ui_state.reconnect_attempted
):
ui_state.reconnect_attempted = True
status_win = show_connection_status(stdscr, "Disconnected", "Trying to reconnect…")
try:
from contact.ui.control_ui import reconnect_interface_with_splash
reconnect_interface_with_splash(stdscr, interface)
if status_win is not None:
status_win.erase()
status_win.refresh()
ui_state.reconnect_attempted = False
handle_resize(stdscr, False)
except Exception:
if status_win is not None:
status_win.erase()
status_win.refresh()
handle_resize(stdscr, False)
logging.exception("Automatic reconnect after transport disconnect failed")
retry = get_list_input(
"Contact could not reconnect after 20 seconds.",
"Retry",
["Retry", "Cancel"],
mandatory=True,
)
if retry == "Retry":
ui_state.reconnect_attempted = False
handle_resize(stdscr, False)
continue
handle_resize(stdscr, False)
with app_state.lock:
process_pending_ui_updates(stdscr)
entry_display = f"{ui_state.reply_context}{input_text or ''}"
@@ -652,12 +687,27 @@ def handle_enter(input_text: str) -> str:
)
return input_text
# Enter key pressed, send user input as message
send_message(
input_text,
channel=ui_state.selected_channel,
reply_id=ui_state.reply_id,
reply_context=ui_state.reply_context,
)
try:
send_message(
input_text,
channel=ui_state.selected_channel,
reply_id=ui_state.reply_id,
reply_context=ui_state.reply_context,
)
except Exception as exc:
logging.warning("Message send failed; reconnecting interface: %s", exc)
try:
from contact.ui.control_ui import reconnect_interface_with_splash
reconnect_interface_with_splash(root_win, interface_state.interface)
contact.ui.dialog.dialog("Reconnected", "Connection restored. Your message was not sent; press Enter to retry.")
except Exception as reconnect_exc:
logging.exception("Automatic reconnect failed")
contact.ui.dialog.dialog(
"Reconnect failed",
"Contact could not reconnect after 20 seconds. Check the radio connection and retry your message.",
)
return input_text
ui_state.reply_id = None
ui_state.reply_context = ""
ui_state.reply_id_unavailable = False
@@ -1012,6 +1062,23 @@ def show_remote_admin_wait(stdscr: curses.window, node_name: str) -> curses.wind
return None
def show_connection_status(stdscr: curses.window, title: str, message: str) -> curses.window | None:
"""Display a non-blocking connection-status dialog over the current UI."""
try:
height, width = stdscr.getmaxyx()
box_width = min(width - 4, max(len(message) + 4, len(title) + 6, 32))
win = curses.newwin(5, box_width, max(0, (height - 5) // 2), max(0, (width - box_width) // 2))
win.bkgd(get_color("background"))
win.attrset(get_color("window_frame"))
win.border()
win.addstr(0, 2, f" {title} ", get_color("settings_default"))
win.addstr(2, 2, message[: box_width - 4], get_color("settings_default"))
win.refresh()
return win
except curses.error:
return None
def update_remote_admin_wait(wait_win: curses.window | None, message: str) -> None:
"""Append the current remote-admin request to the waiting overlay."""
if wait_win is None:
-4
View File
@@ -26,7 +26,6 @@ from contact.ui.colors import get_color
from contact.ui.dialog import dialog
from contact.ui.menus import generate_menu_from_protobuf
from contact.ui.nav_utils import move_highlight, draw_arrows, update_help_window
from contact.ui.splash import draw_splash
from contact.ui.user_config import json_editor
from contact.utilities.arg_parser import setup_parser
from contact.utilities.singleton import interface_state, menu_state
@@ -236,9 +235,6 @@ def reconnect_interface_with_splash(stdscr: object, interface: object) -> object
except Exception:
pass
stdscr.clear()
stdscr.refresh()
draw_splash(stdscr)
new_interface = reconnect_interface(setup_parser().parse_args())
interface_state.interface = new_interface
redraw_main_ui_after_reconnect(stdscr)
+1
View File
@@ -39,6 +39,7 @@ class ChatUIState:
redraw_full_ui: bool = False
scroll_messages_to_bottom: bool = False
preserve_message_selection: bool = False
reconnect_attempted: bool = False
oldest_message_rowid: Dict[Union[str, int], int] = field(default_factory=dict)
has_older_messages: Dict[Union[str, int], bool] = field(default_factory=dict)
message_line_ranges: Dict[Union[str, int], List[tuple]] = field(default_factory=dict)
+9 -3
View File
@@ -44,15 +44,21 @@ def initialize_interface(args):
logging.critical(f"Fatal error initializing interface: {ex}")
def reconnect_interface(args, attempts: int = 15, delay_seconds: float = 1.0):
def reconnect_interface(args, attempts: int = 20, delay_seconds: float = 1.0):
last_error = None
for attempt in range(attempts):
try:
interface = initialize_interface(args)
if interface is not None:
if interface is not None and getattr(interface, "localNode", None) is not None and getattr(
interface.localNode, "localConfig", None
) is not None and (not hasattr(interface, "stream") or interface.stream is not None):
return interface
last_error = RuntimeError("initialize_interface returned None")
last_error = RuntimeError("interface did not complete connection setup")
try:
interface.close()
except Exception:
pass
except Exception as ex:
last_error = ex