1275 Commits
Author SHA1 Message Date
Lloyd 47e49e64aa Merge pull request #463 from openhop-dev/main
fix legacy restart interrupted.
2026-09-09 00:22:36 +01:00
Lloyd 13eb8b2ea8 Merge pull request #462 from openhop-dev/fix-restart-fix
Fix restart fix
1.1.4
2026-09-09 00:01:52 +01:00
Rightup 7dddebde73 test: add case for legacy restart interrupted being likely successful 2026-09-08 23:36:15 +01:00
Rightup fb92b0c4eb feat: implement legacy restart fallback for non-root service restarts 2026-09-08 23:13:07 +01:00
Lloyd b730e9eda9 Merge pull request #461 from openhop-dev/main
docker and version api changes
2026-09-08 22:58:29 +01:00
Lloyd 3a291a1b94 Merge pull request #460 from yellowcooln/fix/main-tagged-docker-release
ci: publish main Docker images only from stable release tags
1.1.3
2026-09-08 22:36:49 +01:00
yellowcooln 883022c7e5 style: format Docker release regression tests 2026-09-08 17:31:28 -04:00
yellowcooln 582ef4d8e7 ci: publish main Docker images only from stable release tags 2026-09-08 17:27:45 -04:00
Rightup fd2571b6e7 fix: ensure exact tags resolve correctly in version fetching logic 2026-09-08 22:26:15 +01:00
Lloyd d0a34d16e2 Merge pull request #459 from openhop-dev/dev
Merge dev into main: radio, plugins, routing and platform updates
1.1.2
2026-09-08 22:00:16 +01:00
Rightup 55e841189a fix: pin openhop_core dependencies to version 1.1.3 and update related documentation 2026-09-08 21:42:02 +01:00
Rightup 28dcfa15a0 feat: Added a QR action next to each neighbor pubkey 2026-09-08 20:48:59 +01:00
Lloyd ffd239dd82 Merge pull request #458 from yellowcooln/fix/sync-stock-meshcore-presets 2026-09-08 20:18:16 +01:00
yellowcooln 547c178268 docs: clarify upstream source in preset info message 2026-09-08 15:13:45 -04:00
yellowcooln 5fcb457f24 fix: synchronize radio presets with stock MeshCore
Match the preset list from MeshCore's official configuration feed one-to-one, including upstream ordering and preset metadata.
2026-09-08 15:09:55 -04:00
Lloyd 0e726f0700 Merge pull request #456 from openhop-dev/merge-pr-454-455
Merge pr 454 455
2026-09-08 17:16:51 +01:00
Rightup cc4be95df1 Merge branch 'pr-455' into merge-pr-454-455 2026-09-08 16:39:19 +01:00
Rightup ef1355f0e8 Merge branch 'pr-454' into merge-pr-454-455 2026-09-08 16:39:19 +01:00
yellowcooln 054ddd0784 fix(manage): render container warning with real line breaks 2026-09-08 11:22:07 -04:00
yellowcooln e4276b4273 fix(service): schedule restarts outside the daemon process group 2026-09-08 10:26:11 -04:00
kyleyank 004723d7cf Add Philadelphia preset 2026-09-08 09:33:43 -04:00
yellowcooln e47a68e021 fix(auth): bound and expire login throttle state 2026-09-08 09:29:31 -04:00
yellowcooln 0969335dfc fix(config): preserve persisted and runtime state on save failure 2026-09-08 09:29:31 -04:00
agessaman ab7479c55c fix(companion): re-assert SSE push callbacks on every stream open
The SSE endpoints registered their bridge callbacks once behind a
_callbacks_registered latch. Anything that cleared the bridge's push
callbacks therefore killed the stream permanently — the latch stayed True
while the subscriptions were gone, so no later stream open repaired them.

Cache the callback objects instead and re-register them each time a stream
opens. Bridge registration is idempotent, so the repeat costs nothing while
a subscription that went missing is repaired rather than leaving the stream
silently dead until the daemon restarts.

The clear that made this reachable is fixed in openhop_core, where the
frame server now retracts only its own subscriptions instead of clearing
every callback on each companion client connection. This half is the
belt-and-braces: clear_push_callbacks() remains a legitimate full reset,
and the stream should survive one.
2026-09-07 14:50:47 -07:00
Rightup 5409029a30 fix: fix plugin padding and add toast alert for new updates. 2026-09-07 22:35:29 +01:00
agessaman 6ca305fa6a fix(region-map): resolve mesh.default_region onto the dispatcher default scope
Firmware's ``simple_repeater`` resolves a ``default_scope`` TransportKey at boot
from ``region_map.getDefaultRegion()`` and answers the ``REPLY_SCOPE_DEFAULT``
row with ``sendFloodScoped(default_scope, ...)``. The comment on that branch is
the whole point of it::

    // un-scoped would be dropped at hop 0 by repeaters running flood.max.unscoped=0

We had the config for it -- ``mesh.default_region``, which the web API already
auto-creates as an allow-flood region -- but nothing carried it to
``Dispatcher.default_flood_transport_key``, so it only ever reached adverts and
the neighbours publisher. Core's send-time resolver therefore found no default
and left the reply a plain flood.

The row is reached whenever the request's scope is *unknowable* rather than
un-scoped: a DIRECT request we hold no return path for, or a TRANSPORT_FLOOD
whose code matched no served region. Both are ordinary traffic, and on a mesh
running ``flood.max.unscoped = 0`` both replies died at hop 0 while firmware
would have delivered them.

``resolve_default_scope_key`` resolves through the built RegionMap rather than
hashing the configured name, for parity with ``getDefaultRegion()``, which can
only return a region the map holds: a ``$private`` default gets its stored key
material, and a default naming an unserved region resolves to nothing rather
than to a scope no local Region would match on the way back in. An unset default
and the ``*`` wildcard resolve to None, which is firmware's
``default_scope.isNull()`` row -- a plain flood.

Re-resolved from the three places that change the answer: boot, the
transport_keys change hook, and a runtime ``mesh.default_region`` edit. That last
one matters because the web API creates the region *before* writing the config,
so the storage hook has already run against the previous value.

Companion bridges need no separate wiring: a deferred reply from a bridge's
login server reaches the shared dispatcher unmarked, so it resolves here, which
is what firmware ``simple_room_server`` does with its own ``default_scope``.

The two end-to-end tests skip on a core that still resolves REPLY_SCOPE_DEFAULT
at RX and marks the reply final, since the assignment is correct but inert
there. They are gated on the behaviour rather than on a version, so they start
running by themselves once that core lands.
2026-09-07 22:27:50 +01:00
agessaman 0cecb17060 fix(engine): wait out a radio-link outage instead of spending the retry on it
A local transmit retried after a flat 1s backoff. When the failure was the
link itself -- a KISS modem that dropped off USB and is re-enumerating, which
takes about 2s to come back and re-handshake -- that retry landed inside the
outage and failed immediately, turning one modem hiccup into a failed message
even though the companion client was still waiting.

A transient failure with the link up keeps its 1s backoff outside _tx_lock. A
dropped link is now waited for instead, polled at 100ms and bounded by
delays.local_tx_link_wait_seconds: default 5s, non-finite values rejected and
everything capped at 30s, because the client's own command timeout is already
part-spent on the TX delay by the time we get here. Relayed packets never
wait -- a repeat held back that long is worse than one dropped.

Link state is read from the radio the send will actually use. Attribute
access on a FabricRadio passes through to its default endpoint, so a send
carrying preferred_tx_radio_id would otherwise be judged by the wrong radio,
suppressing a healthy one or waving through a dead one. A multi-radio fabric
with no explicit target picks its endpoint at send time, so nothing is
inspected and the send proceeds as before. Radios that track no link state
(a directly driven SX1262) answer nothing and keep their exact timing.
2026-09-07 22:27:50 +01:00
agessaman 2cf65abf62 fix(room): end a pushed post's text at an embedded NUL
pushPostToClient sizes the body with strlen(post.text) for the payload and
the expected ACK alike. The web API will store "a\0b", and pushing it whole
sends text the receiver cannot see -- it stops at the NUL -- while hashing
the pending ACK over a span the receiver never reproduces. The push would
then never be acknowledged and would retry into the failure backoff.

Same defect the core builder had; this is a second, hand-rolled builder.

Also proves MC-R4 end to end at last: a genuinely encrypted guest PLAIN
through a real registered room identity, watching the injector for an ACK
that must not appear. The seam was only covered in three separate halves
before -- core honours a veto, _may_ack computes one, a mocked constructor
receives one -- none of which would have caught the wiring being dropped.
2026-09-07 22:27:50 +01:00
agessaman 1e61b392b8 fix(text): withhold the ACK for anything a server would refuse
The core text handler answers by BaseChatMesh's rule -- plain and signed
text, never a CLI type -- and it runs before this helper's gates. So a
signed post, a non-admin command or a guest's post was acknowledged and
only then thrown away: the sender saw delivery confirmed for a message
that went nowhere.

_may_ack is firmware's rule for a server, handed to the core handler as
should_ack_fn at register_identity. simple_repeater builds an ACK only
under `if (flags == TXT_TYPE_PLAIN)`, inside a branch already gated on
isAdmin(); simple_room_server sets send_ack only on the non-guest PLAIN
path. Neither answers a CLI type, an unsupported type or a replayed
timestamp -- but both still answer a retry, because firmware suppresses
the work, not the reply. It reads no state it does not also leave alone.

It predicts eligibility, not the outcome: a post that clears the role and
replay checks and is then refused by openHop's own rate limit or a
database error is still ACKed. Those refusals have no firmware
counterpart, so closing that gap needs the store result fed back here.

Guarded by a signature check: passing should_ack_fn to a core that does
not take it raises TypeError out of register_identity, which would leave
the node with no text handler at all. pyproject.toml tracks
openhop_core@dev with no version floor, so that is an ordinary state --
and in it the ACK goes back to being sent, availability over the
invariant.
2026-09-07 22:27:50 +01:00
agessaman 0d50f5feec fix(text): dispatch on the text type and guard replays
Two things firmware does that openHop did not.

Dispatch by type, not by text. simple_room_server::onPeerDataRecv routes
CLI_DATA/CLI_COMMAND to the admin CLI and turns PLAIN into a post;
simple_repeater has no chat function, so it runs every accepted type
through handleCommand with no text test at all. openHop asked
_is_cli_command(message_text) for both, and so got both wrong: a PLAIN
"set ..." ran as a command instead of being posted, a command whose text
was not in the prefix list was published to the room, and an admin's
mistyped repeater command vanished silently instead of being answered.

Guard replays. Both servers open the branch with
`sender_timestamp >= client->last_timestamp`, treat equality as a retry
they accept but do not re-run, and drop anything older. openHop checked
neither, so replaying an admin command with the same timestamp and
different attempt bits re-executed it -- packet dedup does not catch that
because the bytes differ. Note ACL._is_replay is not reusable here: its
strict <= rejects the equal case this branch accepts.

Both read from packet.decrypted, which openhop_core now publishes. A core
too old to report them falls back to the previous text-prefix behaviour
and logs a warning rather than dropping every message.
2026-09-07 22:27:50 +01:00
agessaman e6d1a43d86 fix(room): a guest may read the room, not post to it
simple_room_server::onPeerDataRecv takes the
`(client->permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST` branch for a
PLAIN message and stores nothing. openHop called add_post for anyone who
authenticated, so knowing the guest password was enough to write to a room:
add_post caps length and rate-limits per client, but never looks at the role.

Also warns once per message when openhop_core does not publish txt_type,
so the open text-type gate is visible in the log rather than silent.
2026-09-07 22:27:50 +01:00
agessaman 369c6142b6 fix(text): gate the CLI on the text type, not the text
simple_repeater::onPeerDataRecv and simple_room_server::onPeerDataRecv
both open their TXT_MSG branch by rejecting any flags outside
{PLAIN, CLI_DATA, CLI_COMMAND}. openHop had no such gate: it decided what
a message was purely from whether the text looked like a command, so a
SIGNED_PLAIN -- a room post, whose 4-byte author prefix the core handler
has already stripped, leaving bare text -- ran as an admin CLI command
whenever it happened to start with a command prefix, and was stored as a
post on a room server that firmware would have dropped.

CLI_COMMAND (3) joined the accepted set when firmware split "a CLI
command" out of CLI_DATA (MeshCore 2c0ace25); CLI_DATA stays in it
because that is what every released client still sends.

Reads the type from packet.decrypted, which the core text handler now
publishes. A core too old to report it leaves the gate open rather than
dropping every message.
2026-09-07 22:27:50 +01:00
Rightup aeb749218f fix: dash packet order summary as aggregate details 2026-09-07 21:34:36 +01:00
Treehouse-00 b526343981 feat(api): summarise neighbor link history in buckets 2026-09-07 21:29:48 +01:00
Rightup 0a4c31c4fb fix(manager): Add SSE to UI 2026-09-07 21:29:48 +01:00
Treehouse-00 4d8f8d41bd feat(plugins): stream an install or update's progress over server-sent events 2026-09-07 21:29:48 +01:00
Treehouse-00 89d8a9813f perf(web): send the frontend compressed and cacheable 2026-09-07 21:29:48 +01:00
Rightup b9c73def4a feat: enhance plugin web path resolution and normalization in API endpoints and HTTP server 2026-09-07 21:29:48 +01:00
Adam Gessaman a2f883fd7d Merge pull request #452 from agessaman/ci/ruff-format-dev
ci: reformat two files ruff 0.15.14 would rewrite
2026-09-07 13:15:10 -07:00
agessaman 3efcd37f6f ci: reformat two files ruff 0.15.14 would rewrite
`pre-commit run --all-files` fails on `dev` at the `ruff-format` hook, so every
open pull request is red on a check that has nothing to do with its own diff.
Both files arrived with 72da7ee (pr-436) formatted by an earlier ruff.

Purely cosmetic, and exactly what `ruff format` produces:

- `sqlite_handler.py` — a generator expression and an f-string concatenation
  that both now fit inside the line limit unsplit.
- `test_companion_import_repeater_contacts.py` — the blank line ruff requires
  after a module docstring.
2026-09-07 13:05:19 -07:00
Yellowcooln 726cbf99b9 Merge pull request #448 from openhop-dev/fix-post-upgrade
add post upgrade messege on how to fix plugin + QR code for PUB keys
2026-09-07 12:01:47 -04:00
Rightup 2eaf380d3d fix: add nosec comments for subprocess.run calls in service_utils.py 2026-09-07 16:51:27 +01:00
Rightup 0b98d2b680 add qr code to FE 2026-09-07 13:39:29 +01:00
Rightup 63ce5b4294 fix: include public key in API response for configured room servers 2026-09-07 13:36:04 +01:00
Rightup ab2e79d771 add further ins to update process. 2026-09-07 12:55:53 +01:00
Rightup cc7a1c426d fix: add notes on how to fix the issue. 2026-09-07 12:12:50 +01:00
Rightup b1c9b6f6a5 fix: enhance plugin manager service bootstrap to support sudo execution 2026-09-07 12:06:38 +01:00
Rightup 3afec59c1a feat: implement plugin manager service bootstrap and add tests 2026-09-07 10:31:05 +01:00
Rightup b65e54196b Merge branch 'pr-436' into dev 2026-09-06 20:35:09 +01:00
Rightup c9fddbfd4b fix: update file handling to use UTF-8 encoding for YAML operations 2026-09-06 20:32:43 +01:00