fix(worker): run repeater poll as cancellable background task
WHAT
Run _poll_repeaters() as an asyncio.Task instead of awaiting it in
_main_loop(). The task is cancelled as soon as has_pending_commands()
reports queued work, and a new poll is only started when the queue is
empty. RepeaterPoller.poll_due() catches the CancelledError, resets the
repeater's _next_due to now and re-raises, so a cancelled poll writes no
archive record and is retried on the next tick.
Adds SharedData.has_pending_commands() with its SharedDataWriter protocol
entry, and REPEATER_POLL_CANCEL_TIMEOUT (default 5.0) to bound the logout
cleanup of a cancelled poll. Corrects the comment on
REPEATER_POLL_MAX_ATTEMPTS, which still claimed a poll blocks the loop.
WHY
_main_loop() awaited the poll in the same iteration as process_all(), so
a repeater poll blocked command dispatch for its full duration. With the
1.24.2 defaults an unreachable repeater occupies 5 x 30 s login timeout
plus 4 x 5 s retry delay, roughly 190 s. A bot reply queued in that
window was only transmitted once the poll gave up, and a restart within
the window discarded it because the command queue lives in memory.
Confirmed in journal meshcore-gui-ttyUSB1.service of 2026-09-06: replies
queued at 09:33:46 and 09:35:23 were both sent at 09:36:25, the second
the poll ran out of attempts.
Statistics collection has no deadline; a bot reply does. Shortening the
retry budget would only shorten the window, not remove it.
NOTES
No breaking changes. has_pending_commands() is additive. Archive schema
unchanged, but no record is written for a cancelled poll, so the number
of polls per day can be lower than the interval suggests. A repeater on a
busy channel may go longer without a measurement — the intended trade-off.
Bumps version to 1.24.3.
WHAT
- route_page: _default_reply_channel() derives the reply select default
from msg.channel instead of the first entry in the channel list
- models: new Message.date field, now_date() and display_timestamp();
format_line() and incoming()/outgoing() use them
- message_archive: persist "date" alongside "time" and "timestamp_utc"
- room_server_panel + route_page info header use display_timestamp()
- VERSION 1.22.2 -> 1.22.3
WHY
The route page exists to show one specific message, so that message —
not the ordering of the channel list — is the correct source for the
reply default. The date could not be fixed at the render site because
Message had no date at all: now_timestamp() yields HH:MM:SS and the
date lived only as timestamp_utc inside the archive layer, unread by
from_dict(). A first-class field plus a single formatting helper fixes
the cause and keeps the format in one place.
NOTES
No breaking changes. The archive schema gains one additive field;
pre-1.22.3 entries have their date derived from timestamp_utc
(converted to local time) and fall back to time-only when absent.
Message.incoming() gained an optional keyword-only date parameter.
Message display lines are ~11 characters wider.
### FIXED
- **Multibyte path hash support** (`ble/events.py`, `core/shared_data.py`):
corrected docstrings in both `_resolve_path_names` methods that incorrectly
described path hashes as "2-char hex strings". The actual contact lookup
uses `startswith` matching, which is hash-size agnostic and correctly
handles 1-byte (2 hex chars), 2-byte (4 hex chars) and 3-byte (6 hex chars)
path hashes as introduced in MeshCore firmware v1.14.0. No functional code
was changed — only the documentation was incorrect.
- **MariaDB schema** (`meshcore_schema.sql`): `meshcore_messages.path_hashes`
column widened from `VARCHAR(128)` to `VARCHAR(255)`. The old limit caused
silent truncation for paths longer than ~40 hops in 1-byte mode or ~25 hops
in 2-byte mode. Migration is backward-compatible; existing data is unchanged.
### CHANGED
- `config.py`: version bump `1.17.0 → 1.17.1`.
### RATIONALE
- MeshCore firmware v1.14.0 (2026-03-06) introduced configurable path hash
sizes (1-, 2- or 3-byte per repeater). Verification confirmed that
`meshcoredecoder 0.3.2` already returns correctly sized hex strings via
`_decode_path_len_byte`. The GUI path-resolution logic was already
forward-compatible; only the docstrings and the MariaDB column width required
correction.
### IMPACT
- No BLE handler, GUI panel, service or API endpoint modified.
- `meshcoredecoder` library unchanged; no pip update required.
- MariaDB migration: single `ALTER TABLE` statement, no downtime, no data loss.