The contact submenu's reset-path action set out_path_len = 0, which
means "direct, 0 hops" — formatHopCount rendered the contact as
"direct" and sendMessage would still try direct send. Use
OUT_PATH_UNKNOWN (0xFF) so the next DM floods and rediscovers the
path. Also schedule a contacts flush (markContactsDirtyPublic) so
the reset survives reboot — previously the change was in-memory only.
UIScreen gains onEnter()/onExit() lifecycle hooks; poll() and the
_curr->poll() call in the main loop are removed. Each screen with
periodic or deadline-based work owns its own k_timer:
- one-shot timers: Splash dismiss, Countdown alarm, Contacts ping
timeout, RepeaterAdmin cmd/login timeout, Unread preview expiry
- periodic timers: Snake tick, GPSSettings sample
- onEnter()-only: Repeaters discover, Doom start
- deleted: Home, Stopwatch (were empty)
Timer ISR callbacks only signal _task->notify() — never mutate
screen state. Main-thread render() handles transitions. Setting
_curr now fires onExit on the outgoing screen and onEnter on the
incoming one, so timers are scoped to screen lifetime and can't
fire stale events on the wrong screen.
New 6th item in the repeater admin submenu (admin only). Sends
"clock sync" as a CLI command; the repeater reads sender_timestamp
from the packet metadata and sets its RTC to our companion epoch
if ours is ahead. Response lands in the existing admin history list.