Skip to content

feat(app): send one broker to several collectors under one switch - #722

Merged
efiten merged 2 commits into
efiten:masterfrom
khagele:feat/704-dmc-one-row
Sep 30, 2026
Merged

efiten merged 2 commits into
efiten:masterfrom
khagele:feat/704-dmc-one-row

Conversation

@khagele

@khagele khagele commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Closes #704

What was wrong

DutchMeshCore takes two stream labels on two collectors, and every config.json broker was its own row with its own switch. The site therefore offered four presets, and a hunter picked one collector and one label per drive.

What this changes

  • streams on a config broker. One entry, one row on the brokers page, one line in the Status tab, one switch. legsOf (app/src/brokers.js) turns each stream into a connection with the id <broker>@<host>, so each keeps its own watermark, companion token (signed for its host) and socket. Drain, retention, connect and the startup watermarks now walk connections; rows and switches stay brokers.
  • Row status folds its connections (foldLegs): connected when all are, 1 of 2 connected in between, queued as far as the slowest connection.
  • Brokers page lists each stream as host · label.
  • Add form refuses a host the site already publishes to. The companion's key is the client id on every broker it signs in to, and two sessions under one id kick each other.
  • Splash and About say where receptions go, from the brokers that are on: "Your receptions are shared with Mesh-Hunter and DutchMeshCore." With every broker off: "Your receptions stay on this phone."
  • Docs: docs/2026-09-21-mqtt-brokers.md amended, AGENTS.md §9 and §10, config.example.json, one changelog entry.

Deploy: the site's config.json

The DutchMeshCore hosts stay out of the repo, so the live config.json needs this at deploy, with the collector addresses the current presets use:

{ "id": "dutchmeshcore", "name": "DutchMeshCore", "auth": "companion", "format": "wardrive",
  "streams": [ { "url": "<collector 1>", "label": "hunter" }, { "url": "<collector 2>", "label": "wardriver" } ] }

The four DutchMeshCore entries leave brokerPresets.

Choices worth a look

  • A broker a hunter added from a preset before this is not migrated. After the deploy it shares a host with the site row, so whoever added one removes it once; its row says so. The app is in development, and stored state gets no shims.
  • The row counts queued as the maximum over its connections, not the sum: each drains on its own.

Verification

  • app: 1640 tests (17 new), eslint, build. web: 762 tests (changelog parity), eslint.
  • Every new test mutation-checked: 15 mutations, each red on the test it is about.
  • In the browser against three local brokers: the own one got packets, collector 1 got wardriver/obs under meshcore/hunter/…, collector 2 under meshcore/wardriver/…. With the switch off only the own broker got the next reception; switched on again, both collectors caught up. The add form refused collector 2's host.
  • Not tested: the real collectors, and a companion signing two tokens in a row.

Overlap

app/changelog.json and web/changelog.json are regenerated here, as in #710 to #718. Whichever merges second reruns node scripts/build-changelog.mjs.

🤖 Generated with Claude Code

@khagele
khagele force-pushed the feat/704-dmc-one-row branch from 239237c to 9519f45 Compare September 28, 2026 14:43
@khagele

khagele commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Amended (9519f45), three changes:

App: 1640 tests (17 new), eslint, build. Web: 762 tests, eslint.

@efiten

efiten commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Review before merge: one point to change, one suggestion, three small ones. DutchMeshCore on by default is confirmed on the maintainer side.

1. An existing DutchMeshCore preset keeps signing the new default connection out (change requested)
A hunter who added a DMC preset before this keeps a user:<collector-host> broker that stays on, next to the new site connection dutchmeshcore@<collector-host>. Both connect with clientId: owner (app/src/app.js:2252), so they take turns kicking each other off until the hunter removes theirs. hostClashes (app/src/brokers.js:91) only drives the row message; ensureMqtt (app.js:2190, over legList()) still connects both. On deploy day this reaches every current DMC user.
Proposed: skip clashing user brokers in legList() (for example brokerList().filter(b => !hostClashes(all).has(b.id))), so they stay inert with the "remove this one" hint, plus a test.

2. Suggestion: pin the retention path over connections
Drain, prune and ensureMqtt now iterate connections (legList()/allLegs()) instead of brokers. That is the AGENTS.md §10 watermark invariant, and no test pins it: if prune went back to brokerList() it would read the dutchmeshcore watermark, which never drains, and nothing turns red. A pure owedLegs(brokers) with a test would pin it.

3. Small ones

  • Two streams on the same host inside one broker give the same leg id (brokers.js:80), so they share a publisher and a watermark and one label is lost. normalizeConfig could de-duplicate them as it does broker ids.
  • brokers.js:170: the "N of M connected" branch comes before signRefused/needsCompanion, so with 1 of 2 up and the other refused by the companion, the row hides the reason.
  • row: broker.id on a leg (brokers.js:80) is only read by a test.

DutchMeshCore is one broker with two collectors behind it, and each takes
its own stream label: collector 1 hunter, collector 2 wardriver. Until now
every config.json entry was its own row with its own switch, so the site
offered four presets and a hunter had to pick one per drive.

A config.json broker can now list streams, each with its own url and
label. It stays one entry: one row on the brokers page, one line in the
Status tab, one switch. legsOf makes a connection of each stream under the
id <broker>@<host>, so each keeps its own watermark, companion token and
socket; the drain, retention and connect code now walk connections, the
rows and switches stay brokers. A row's status folds its connections
("1 of 2 connected" in between). The add form refuses a host the site
already publishes to, since two sessions under the companion's key kick
each other. The splash and About say where receptions go, from the
brokers that are on.

Verified: 1637 app and 762 web tests, eslint, build; every new test
mutation-checked. In the browser against three local brokers: the own one
got packets, collector 1 obs under hunter, collector 2 under wardriver;
with the switch off only the own one got the next reception, and on again
both collectors caught up. Not tested against the real collectors or with
a companion signing.

Closes efiten#704

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@khagele
khagele force-pushed the feat/704-dmc-one-row branch from 9519f45 to ee69e1e Compare September 30, 2026 17:54
@khagele

khagele commented Sep 30, 2026

Copy link
Copy Markdown
Contributor Author

Pushed ee69e1e for the review, rebased on master. Per point:

  1. A clashing preset signs the default out. liveBrokers in brokers.js leaves a user broker on a site broker's host out of brokerList() and allLegs(), so ensureMqtt never connects it and retention does not wait for it. Its row keeps the "Same host as DutchMeshCore: remove this one" line. Test: the site broker and a user broker on its own host stay, the clashing one goes. Red with the filter removed.
  2. Retention over connections. owedLegs(brokers) is what pruneOnce asks: the connections of the live brokers that are on. Tests: a site broker with two streams owes dmc@c1.example and dmc@c2.example:8443, never dmc, and a clashing broker is not owed. Red when it goes back to the rows. AGENTS.md §10 names it.
  3. Small ones.
    • normalizeConfig keeps one stream per host (URL.host, the port counts), the first one. Tested.
    • "1 of 2 connected" carries the reason: "· your companion could not sign in" or "· connect your companion to sign in", before the queue count. Tested.
    • row is gone from the legs, and its test with it.

App: 1650 tests, eslint, build. Web: 762 tests, eslint.

@efiten
efiten merged commit 8c6da30 into efiten:master Sep 30, 2026
6 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 30, 2026
efiten pushed a commit that referenced this pull request Sep 30, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>app: 1.30.0</summary>

##
[1.30.0](app-v1.29.1...app-v1.30.0)
(2026-09-30)


### Features

* **app,web:** ease what the map shows by zoom, on one hex system
([#734](#734))
([4c2a57b](4c2a57b))
* **app,web:** name a relay on the map the way the app does
([#733](#733))
([a9aa01a](a9aa01a))
* **app:** send one broker to several collectors under one switch
([#722](#722))
([8c6da30](8c6da30))
* **web:** export the repeaters heard in view as a 1200×1200 PNG
([#725](#725))
([d6186bc](d6186bc))


### Bug Fixes

* **app,web:** hang a Discover-heard repeater's star from its own
position ([#724](#724))
([15a6ada](15a6ada))
* **app,web:** put the caret in the target picker's search when it opens
([#721](#721))
([6bd899c](6bd899c))
* **app:** drop the float window's previous and next buttons
([#717](#717))
([c957ff3](c957ff3))
* **app:** give the sound the playback buffer instead of the smallest
([#712](#712))
([dee5484](dee5484))
* **app:** hide Chrome's pause on the fullscreen readout
([#715](#715))
([836d91e](836d91e))
* **app:** keep a space under the HUD on Android
([#710](#710))
([2754716](2754716))
* **app:** let the fullscreen readout turn with the phone
([#718](#718))
([fae3749](fae3749))
* **app:** put the transmit pop under the receptions it sat over
([#738](#738))
([2bfc5fa](2bfc5fa))
* **server:** keep the node registry in memory on a loop of its own
([#736](#736))
([4f3e988](4f3e988))
* **web:** keep the map bar on one row between 641 and 767 px
([#732](#732))
([c44dc1d](c44dc1d))
</details>

<details><summary>server: 1.10.1</summary>

##
[1.10.1](server-v1.10.0...server-v1.10.1)
(2026-09-30)


### Bug Fixes

* **server:** keep the node registry in memory on a loop of its own
([#736](#736))
([4f3e988](4f3e988))
* **server:** read a refused frame again when the message-id decoder
changes ([#737](#737))
([8586c71](8586c71))
</details>

<details><summary>web: 1.26.0</summary>

##
[1.26.0](web-v1.25.1...web-v1.26.0)
(2026-09-30)


### Features

* **app,web:** ease what the map shows by zoom, on one hex system
([#734](#734))
([4c2a57b](4c2a57b))
* **app,web:** name a relay on the map the way the app does
([#733](#733))
([a9aa01a](a9aa01a))
* **app:** send one broker to several collectors under one switch
([#722](#722))
([8c6da30](8c6da30))
* **web:** export the repeaters heard in view as a 1200×1200 PNG
([#725](#725))
([d6186bc](d6186bc))


### Bug Fixes

* **app,web:** hang a Discover-heard repeater's star from its own
position ([#724](#724))
([15a6ada](15a6ada))
* **app,web:** put the caret in the target picker's search when it opens
([#721](#721))
([6bd899c](6bd899c))
* **app:** drop the float window's previous and next buttons
([#717](#717))
([c957ff3](c957ff3))
* **app:** give the sound the playback buffer instead of the smallest
([#712](#712))
([dee5484](dee5484))
* **app:** hide Chrome's pause on the fullscreen readout
([#715](#715))
([836d91e](836d91e))
* **app:** keep a space under the HUD on Android
([#710](#710))
([2754716](2754716))
* **app:** let the fullscreen readout turn with the phone
([#718](#718))
([fae3749](fae3749))
* **app:** put the transmit pop under the receptions it sat over
([#738](#738))
([2bfc5fa](2bfc5fa))
* **server:** keep the node registry in memory on a loop of its own
([#736](#736))
([4f3e988](4f3e988))
* **web:** keep the map bar on one row between 641 and 767 px
([#732](#732))
([c44dc1d](c44dc1d))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

app: one DutchMeshCore preset per label, with collector 2 as failover for collector 1

2 participants