luci-app-openthread: add new application - #8871
Conversation
2bbe9b9 to
f1c07d3
Compare
openwrt-ai
left a comment
There was a problem hiding this comment.
Reviewed the new package (1 commit). The rpcd ucode backend and ACL/menu wiring look sound: every exposed luci.openthread method is ACL-gated, the raw otbr ubus object is not re-exposed, and secret-returning calls (settings_get, get_networkkey) sit under the write grant. Backend coupling verified where possible — threadstart/threadstop match the otbr ubus object used by the openthread-br proto script; the remaining otbr-agent method names live in ot-br-posix (not in the pre-cloned trees) and per the PR body were cross-checked against the daemon. The same-named-package conflict with the old in-feed luci-app-openthread is already tracked in the PR description (openwrt/packages#30091). A few inline nits/questions below.
Generated by Claude Code
015144a to
4c759f2
Compare
4c759f2 to
24ec23c
Compare
The LuCI web interface for OpenThread Border Router is proposed for the openwrt/luci repository as luci-app-openthread (openwrt/luci#8871), following the standard convention for LuCI apps, as requested in openwrt#29791 and openthread/ot-br-posix#3431. Remove the luci-app-openthread subpackage (and its install recipe) so the UI is no longer shipped from two places. Split out of openwrt#30091 so it can be reviewed and land independently of the 2026.07.0 update. Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Fable 5
The LuCI web interface for OpenThread Border Router is proposed for the openwrt/luci repository as luci-app-openthread (openwrt/luci#8871), following the standard convention for LuCI apps, as requested in openwrt#29791 and openthread/ot-br-posix#3431. Remove the luci-app-openthread subpackage (and its install recipe) so the UI is no longer shipped from two places. Split out of openwrt#30091 so it can be reviewed and land independently of the 2026.07.0 update. Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Fable 5
|
I'd appreciate if someone could approve the CI workflows here :) |
openwrt-ai
left a comment
There was a problem hiding this comment.
Re-reviewed the changes since f1c07d3 (branch was amended into a single commit, 24ec23c). All three earlier threads are addressed correctly:
add.js—value→placeholderon both joiner fields, plus a PSKd check whose character class ([0-9A-HJ-NPR-Y]{6,32}) matches the base32-thread alphabet described in the field's own help text.settings.js/view.js— the hardcodedwpan0is gone; both titles now renderinterfacename. Verified the backing ubus method exists: the legacy app read the samethreadget("interfacename").InterfaceNamepair (102-openwrt-fix-LuCI-icons.patch:20in theopenthread-brfeed package), andsettings_getwas extended to return it alongside the existingstate_summaryfield.luci.openthread— the capital-Errornormalization looks right, andjoin.jsdroppingget_networkkeyis a clean removal: the method and its ACL entry are both gone and no caller remains (git grep get_networkkeyis empty).
openthread.pot was regenerated consistently with the moved line numbers and the two reworded %s (%s) titles. CI has no completed checks on this head (workflow approval still pending), so nothing to ground against there.
Two follow-ups inline — one nit on validation parity, one question about error propagation on the read paths.
Generated by Claude Code
24ec23c to
233d49f
Compare
The LuCI web interface for OpenThread Border Router is proposed for the openwrt/luci repository as luci-app-openthread (openwrt/luci#8871), following the standard convention for LuCI apps, as requested in openwrt#29791 and openthread/ot-br-posix#3431. Remove the luci-app-openthread subpackage (and its install recipe) so the UI is no longer shipped from two places. Split out of openwrt#30091 so it can be reviewed and land independently of the 2026.07.0 update. Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Fable 5
openwrt-ai
left a comment
There was a problem hiding this comment.
Re-reviewed the changes since 24ec23c (branch amended again into a single commit, 233d49c). Both threads from the last round are addressed:
add.js— EUI-64 check taken as suggested, now/^[0-9a-f]{16}$/i, matching the hex promise in its own error message and the checks injoin.js/settings.js.luci.openthread— all three read paths now bail out with{ error }instead of synthesizing a partial reply.summarize_statedropping thes?.optional chaining is safe:otbr()never returns null, it substitutes{ error: 255 }.settings_getreusing thestatereply instead of callingotbr('state')twice is a nice side effect. Theif (s.error)guard also behaves correctly on the normal path, since otbr-agent'sError: 0normalizes to a falsyerror.
No new translatable strings in this round, so openthread.pot is still consistent. No CI checks have run against this head (workflow approval still pending), so nothing to ground against there.
Three follow-ups inline, all about how far the error now propagates: the settings.js error path still renders a positive "network is enabled" claim, view.js is the one read consumer that did not get the new notify(), and the overview's 2 s poll bypasses it.
Generated by Claude Code
233d49f to
0dcade0
Compare
openwrt-ai
left a comment
There was a problem hiding this comment.
Re-reviewed the changes since 233d49c (branch amended again into a single commit, 0dcade0). Diff is three view files plus the regenerated template. All three threads from the last round are addressed:
settings.js—render()now readss.errorfirst and returns an error-only card, so the "Thread network is enabled" claim, the live Disable, the mode hint andLEAVEare all unreachable on a failed read. The stale comment about blank inputs is gone too.view.js— same treatment, plus theotbrErrors.notify(st)that was missing.overview.js— the poll blind spot is handled the right way round:renderStatusRow()itself renders the unreachable row, so the 2 s tick surfaces a daemon that dies mid-session without toast spam, and the load path keeps its one-shotnotify().
alert-message warning matches the dominant in-tree class for this kind of card (18 uses across applications/), and otbrErrors.translate(x) || x degrades correctly for a non-numeric code. Checked the regenerated openthread.pot mechanically: every #: reference resolves to a line that actually contains its msgid at 0dcade0, and the new otbr-agent is not reachable (%s) string is listed against all three call sites.
CI on this head: the three FormalityCheck runs pass; the build workflow is still awaiting maintainer approval, so nothing to ground against there.
One follow-up inline — view.js has the same poll-vs-load-path asymmetry that overview.js just fixed.
Generated by Claude Code
0dcade0 to
83b422c
Compare
openwrt-ai
left a comment
There was a problem hiding this comment.
Re-reviewed the changes since 0dcade0 (branch amended again into a single commit, 83b422c). Diff is view.js plus the regenerated template. The thread from the last round is addressed the way it was framed:
renderLeaderTableandrenderNeighborsTableeach handleerrorthemselves and render an unreachable row, so the 2 s poll surfaces a daemon that dies mid-session — matching therenderStatusRowtreatment inoverview.js, comment and all.- The early return in
render()is gone, sopoll.addis now always reached: a load against a dead otbr-agent no longer strands the page until a manual reload. That was the more consequential half of the last comment. - The heading falls back to the plain
Thread Viewwhenst.erroris set, so theThread View: ()rendering can't occur. otbrErrors.notify(st)is kept above as the one-shot load-time signal, so there is no per-tick toast spam.
Checked the regenerated openthread.pot mechanically: every #: reference resolves to a line that actually contains its msgid at 83b422c (spot-checked all 24 changed view.js references), no msgids were added or removed, and the existing otbr-agent is not reachable (%s) entry correctly gained the two new call sites at view.js:40 and view.js:87 alongside the three older ones.
CI on this head: the three FormalityCheck runs pass; the build workflow is still awaiting maintainer approval, so nothing to ground against there.
Two follow-ups inline, both cosmetic — a width style that the new error branch dropped, and the heading being the one node the poll doesn't refresh.
Generated by Claude Code
|
|
||
| function renderNeighborsTable(neigh) { | ||
| if (neigh && neigh.error) { | ||
| return E('table', { 'class': 'table' }, [ |
There was a problem hiding this comment.
nit: this error table drops the 'style': 'width:90%;margin-left:5%' that the normal return of the same function carries at view.js:129.`` Its wrapper neighborsBox is a bare `E('div', {})`, so the width comes from the table itself — meaning the unreachable row renders full-bleed and left-flush while the leader box above it stays inset at 5% (that one gets its width from the `cbi-map` wrapper, so its error branch is unaffected). Since the poll swaps between the two variants every 2 s, the section visibly jumps in and out of alignment as the daemon goes down and comes back.
| return E('table', { 'class': 'table' }, [ | |
| return E('table', { 'class': 'table', 'style': 'width:90%;margin-left:5%' }, [ |
Generated by Claude Code
| E('h2', {}, [ st.error | ||
| ? _('Thread View') | ||
| : _('Thread View: %s (%s)').format(st.networkname || '', st.interfacename || '') ]), |
There was a problem hiding this comment.
nit: this heading is now the only part of the page that the poll does not refresh. leaderBox and neighborsBox are re-rendered every 2 s by the dom.content() calls at view.js:169-174,`` but the h2 is built once from the load-time `st`, so both directions of the transition end up inconsistent:
- daemon dies mid-session → stale
Thread View: MyNet (wpan0)sits above two "otbr-agent is not reachable" tables; - load failed, daemon comes back → tables repopulate but the title stays the bare
Thread View, which is exactly the self-repair the early return was dropped for.
Keeping a reference to the title node and dom.content()-ing it from the poll callback next to the other two boxes would put it back in step. Purely cosmetic either way — the tables already carry the real signal.
Generated by Claude Code
833b3cd to
19d2773
Compare
5e3a874 to
711a5de
Compare
Add a LuCI web interface for the OpenThread Border Router (openthread-br package): network overview with a live neighbor table, network scan and join, network settings including MAC-filter management, joiner commissioning, and a network topology list view. The application is client-side JS (view.extend / rpc.declare) with an rpcd ucode backend (luci.openthread) that brokers ACL-gated access to otbr-agent's ubus API; the raw otbr ubus object is not exposed to LuCI sessions. This migrates the app out of the ot-br-posix repository, where a legacy Lua/htm version was previously maintained under src/openwrt and packaged by the openthread-br feed package, per the maintainers' request in openthread/ot-br-posix#3431 and openwrt/packages#29791. The UI has been rewritten for the modern client-side LuCI framework as part of the move. Signed-off-by: Christian Glombek <c.glombek@cosa.systems> Assisted-By: Claude Fable 5
711a5de to
7f9db59
Compare
openwrt-ai
left a comment
There was a problem hiding this comment.
Re-reviewed the changes since 83b422c (branch amended again into a single commit, 7f9db59). This round is a restructure rather than a fixup: the five separate views and their menu entries are gone, everything is now the single overview page with the former pages as modals, and the backend gained radio_restart, secret_get, Active Dataset TLV decoding, a deferred scan and SRP host-name lookup via ot-ctl. Both nits from the last round are moot — view.js no longer exists.
What I checked on the new surface:
- Secrets.
settings_getno longer hands outnetworkkey/pskc; they moved behindsecret_get, which is in thewritegrant, as isradio_restart. The read grant still only coversstate_summary,neighbors_summaryandscan— with one exception I've raised inline (the MAC filter list is now part of the read-gated neighbour summary). - Deferred scan. Matches the sanctioned rpcd pattern — return the
ubus.defer()handle and reply from the completion callback (example-plugin.uc:108-122), which rpcd recognises via theubus.deferredresource check inucode.c:463and guards with its own timeout. Same shape as the in-treeluci.upnp. The!dfallback covers the defer-failed case. radio_restart. The proto match is right: the feed registersadd_protocol openthread(openthread-proto.sh:107), and/usr/sbin/ot-ctlis the same path that script uses, so theopenthread-brdependency in the Makefile covers the new CLI reads.- Dataset decoding. The TLV walk is bounded on both ends (
i + 4 <= n, truncated-value break), and the Channel TLV correctly skips the channel-page byte before taking the 2-byte channel. - Translations.
openthread.potis regenerated consistently: all 157#:references resolve to a line that actually contains their msgid at 7f9db59. - CI on this head. The three FormalityCheck runs pass; the build workflow is still awaiting maintainer approval, so nothing to ground against there.
Five follow-ups inline. The first one is not cosmetic: in allowlist (and disable) mode the new per-neighbour action reads the filter list as if it were always a deny list, so allowlisted devices get an Unblock button that in fact removes them from the allowlist.
Generated by Claude Code
| if (!bss.ExtAddress) | ||
| return ''; | ||
|
|
||
| var blocked = (neigh.blocked || []).indexOf(bss.ExtAddress) >= 0; |
There was a problem hiding this comment.
neigh.blocked is the raw macfilteraddr.addrlist, returned regardless of the filter mode (luci.openthread:184-185)`` — in allowlist mode that list is the allow list. Since this check runs before the `neigh.macfilter == 'allowlist'` guard below, every allowlisted neighbour renders an Unblock button whose handler calls `macfilter_remove(addr, 'allowlist')`, i.e. drops the device from the allowlist and thereby blocks it. The guard on line 836 is unreachable for exactly the devices it was written for. The `disable` case has the same shape: the daemon keeps its entries while the filter is off, so a leftover address shows "Unblock" although nothing is being filtered.
| var blocked = (neigh.blocked || []).indexOf(bss.ExtAddress) >= 0; | |
| var blocked = (neigh.macfilter == 'denylist') && | |
| (neigh.blocked || []).indexOf(bss.ExtAddress) >= 0; |
Worth double-checking the letter case too: this is an exact indexOf match between bss.ExtAddress from the neighbour list and the entries of addrlist, so the two ubus replies have to agree on case for a blocked device to be recognised at all.
Generated by Claude Code
| // The MAC filter is how a node can be kept off this border router; | ||
| // report it alongside the neighbours so the overview can offer it | ||
| // without the settings read, which is privileged (it exposes keys). | ||
| result.macfilter = otbr('macfilterstate').state; | ||
| result.blocked = otbr('macfilteraddr').addrlist ?? []; |
There was a problem hiding this comment.
This moves the MAC filter state and the full filter address list from the write-gated settings_get into the read-gated neighbors_summary, so a session holding only the read grant of luci-app-openthread now gets the filter contents (which devices the operator excluded, or in allowlist mode the complete roster of permitted EUI-64s).
The stated justification no longer holds after this same commit: settings_get does not expose keys any more — networkkey/pskc moved to the write-gated secret_get a few lines further down. So "the settings read, which is privileged (it exposes keys)" describes the previous revision.
Is the read-side exposure intended? If the overview needs the filter only to label the Block/Unblock buttons, keeping it in the (now key-free, still write-gated) settings_get would avoid widening the read grant; if it is intended, the comment should say so rather than cite the key exposure.
Generated by Claude Code
| function otctl(args) { | ||
| // The SRP registrations are not exposed over ubus; read them from the | ||
| // CLI socket the same way ot-ctl does. Root-only rpcd context. | ||
| let fs = require('fs'); | ||
| let bin = fs.access('/usr/sbin/ot-ctl', 'x') ? '/usr/sbin/ot-ctl' : 'ot-ctl'; | ||
| return fs.popen(bin + ' ' + args, 'r'); | ||
| } |
There was a problem hiding this comment.
Two questions about this helper, both because it sits on the polled path: summarize_neighbors is what the overview's 2 s poll calls (overview.js:962-968).``
-
Cost. With at least one neighbour present, every tick forks
ot-ctl srp server host, and if the SRP server has any host, a secondot-ctl childip— two process spawns every 2 s per open browser page, on top of the dozen-odd ubus round-trips the two summaries already make. The host names change on the order of minutes, not seconds; caching the map with a short TTL, or building it only on the load path and reusing it across ticks, would keep the poll cheap on the low-end hardware this typically runs on. -
Interface.
ot-ctlis invoked with no interface argument, so it uses whatever CLI socket it defaults to, while the rest of this file deliberately reports the daemon'sinterfacenameinstead of assumingwpan0(that was the fix in an earlier round). Counter-evidence that this may be fine: the feed's own proto script also calls it bare (openthread-proto.sh:22). But if otbr-agent can be run on a non-default interface, these two reads would silently come back empty while every ubus-sourced field on the page keeps working — theHostcolumn would just show?with no hint why. Passing the known interface name explicitly would remove the question.
Generated by Claude Code
| // Scan and join run as modals on this page, mirroring the wireless | ||
| // overview. Unlike wireless, a Thread active scan is a single multi-second | ||
| // operation (deferred at the rpcd layer), so the modal shows a spinner and | ||
| // offers an explicit repeat instead of a continuous refresh. |
There was a problem hiding this comment.
nit: this describes the opposite of what the modal does. handleScan registers poll.add(state.pollFn, 5) (line 316), so the scan repeats every 5 s for as long as the modal is open, results accumulate in state.cache and stale entries are dimmed rather than dropped — which is what the comment at line 313 says ("Scan continuously while the modal is open, like the wireless scan"). There is no explicit repeat control; the modal's only button is Dismiss.
| // Scan and join run as modals on this page, mirroring the wireless | |
| // overview. Unlike wireless, a Thread active scan is a single multi-second | |
| // operation (deferred at the rpcd layer), so the modal shows a spinner and | |
| // offers an explicit repeat instead of a continuous refresh. | |
| // Scan and join run as modals on this page, mirroring the wireless | |
| // overview. A Thread active scan is a single multi-second operation | |
| // (deferred at the rpcd layer), so the modal shows a spinner until the | |
| // first scan returns and then rescans every 5s, accumulating results. |
Generated by Claude Code
| E('button', { | ||
| 'class': 'cbi-button cbi-button-add', | ||
| 'click': ui.createHandlerFn({}, function() { | ||
| if (macAddInput.value.length != 16) { |
There was a problem hiding this comment.
nit: length-only, so zzzzzzzzzzzzzzzz is accepted and forwarded to macfilteradd, even though the error message on the next line already promises "16 hex characters". Every other input in this file checks the character class — extended PAN ID at line 500, network key at 507, PSKc at 510, the joiner EUI-64 at 568 — and this is the same EUI-64 the joiner field validates as hex.
| if (macAddInput.value.length != 16) { | |
| if (!/^[0-9a-f]{16}$/i.test(macAddInput.value)) { |
Generated by Claude Code
Adds a LuCI web interface for the OpenThread Border Router (
openthread-br),proposed to be moved out of the ot-br-posix tree at the maintainers' request
(openthread/ot-br-posix#3431, openwrt/packages#29791) and rewritten for the
modern client-side LuCI framework.
Features: network overview with a live neighbor table, network scan & join,
network settings (channel/PAN ID/keys) including MAC-filter management, joiner
commissioning, and a topology list view.
Architecture: client-side JS views (
view.extend/rpc.declare/poll/ui/dom) over an rpcd ucode backend (luci.openthread) that brokersACL-gated access to otbr-agent's ubus API. The raw
otbrubus object is notexposed to LuCI sessions. Depends on
rpcd-mod-ucode.The app reads otbr-agent's existing ubus reply fields, so it works against the
currently packaged
openthread-br— no ot-br-posix change is required.Coordination
luci-app-openthreadfrom theopenthread-brfeed. That should land togetherwith this so the two feeds don't ship a same-named package.
there.
Both are still proposals under review; nothing has been removed anywhere yet.
The app reads otbr-agent's existing ubus reply fields, so no ot-br-posix change is
required for it to work. Note otbr-agent returns its
otErrorcode in a capitalErrorfield; the ucode backend maps it to lowercaseerrorinternally, so nodaemon-side change is implied by that either.
Interface names shown in the UI come from otbr-agent's
interfacenamereplyrather than a hardcoded
wpan0.On access control: the read ACL covers status, neighbour and scan methods only.
Every method that can expose or change credentials — network key, PSKc, MAC
filter, joiner management — requires the write ACL. Form inputs use
non-functional placeholders rather than example values, and the PSKd, EUI-64 and
network-key fields are validity-checked client-side before any call is issued.
Testing
Syntax/convention validation: JS/ucode parse, ACL/menu JSON valid,
i18n-scantemplate generated,
luci.mkapp layout, and every ubus reply field the app readscross-checked against the names otbr-agent's ubus server actually emits. Not yet
exercised on a live device.