Skip to content

luci-app-openthread: add new application - #8871

Open
LorbusChris wants to merge 1 commit into
openwrt:masterfrom
LorbusChris:luci-app-openthread
Open

luci-app-openthread: add new application#8871
LorbusChris wants to merge 1 commit into
openwrt:masterfrom
LorbusChris:luci-app-openthread

Conversation

@LorbusChris

@LorbusChris LorbusChris commented Jul 25, 2026

Copy link
Copy Markdown

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, and
joiner commissioning.

Architecture: client-side JS views (view.extend / rpc.declare / poll /
ui / dom) over 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. 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

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 otError code in a capital
Error field; the ucode backend maps it to lowercase error internally, so no
daemon-side change is implied by that either.

Interface names shown in the UI come from otbr-agent's interfacename reply
rather 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-scan
template generated, luci.mk app layout, and every ubus reply field the app reads
cross-checked against the names otbr-agent's ubus server actually emits. Not yet
exercised on a live device.

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch 2 times, most recently from 015144a to 4c759f2 Compare July 25, 2026 17:24
@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch from 4c759f2 to 24ec23c Compare July 25, 2026 22:18
LorbusChris added a commit to LorbusChris/openwrt-packages that referenced this pull request Jul 25, 2026
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
LorbusChris added a commit to LorbusChris/openwrt-packages that referenced this pull request Jul 25, 2026
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
@LorbusChris

Copy link
Copy Markdown
Author

I'd appreciate if someone could approve the CI workflows here :)

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since f1c07d3 (branch was amended into a single commit, 24ec23c). All three earlier threads are addressed correctly:

  • add.jsvalueplaceholder on 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 hardcoded wpan0 is gone; both titles now render interfacename. Verified the backing ubus method exists: the legacy app read the same threadget("interfacename").InterfaceName pair (102-openwrt-fix-LuCI-icons.patch:20 in the openthread-br feed package), and settings_get was extended to return it alongside the existing state_summary field.
  • luci.openthread — the capital-Error normalization looks right, and join.js dropping get_networkkey is a clean removal: the method and its ACL entry are both gone and no caller remains (git grep get_networkkey is 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

@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch from 24ec23c to 233d49f Compare July 26, 2026 06:15
LorbusChris added a commit to LorbusChris/openwrt-packages that referenced this pull request Jul 26, 2026
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 openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in join.js / settings.js.
  • luci.openthread — all three read paths now bail out with { error } instead of synthesizing a partial reply. summarize_state dropping the s?. optional chaining is safe: otbr() never returns null, it substitutes { error: 255 }. settings_get reusing the state reply instead of calling otbr('state') twice is a nice side effect. The if (s.error) guard also behaves correctly on the normal path, since otbr-agent's Error: 0 normalizes to a falsy error.

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

@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch from 233d49f to 0dcade0 Compare July 26, 2026 20:25

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.jsrender() now reads s.error first and returns an error-only card, so the "Thread network is enabled" claim, the live Disable, the mode hint and LEAVE are all unreachable on a failed read. The stale comment about blank inputs is gone too.
  • view.js — same treatment, plus the otbrErrors.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-shot notify().

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

@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch from 0dcade0 to 83b422c Compare July 27, 2026 17:13

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • renderLeaderTable and renderNeighborsTable each handle error themselves and render an unreachable row, so the 2 s poll surfaces a daemon that dies mid-session — matching the renderStatusRow treatment in overview.js, comment and all.
  • The early return in render() is gone, so poll.add is 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 View when st.error is set, so the Thread 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

@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch 6 times, most recently from 833b3cd to 19d2773 Compare July 28, 2026 23:27
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since a2d15f4 (branch amended again into a single commit, 2ae1f9d). Diff is overview.js only — 55 insertions, 37 deletions, no backend change. Both threads from the last round are addressed:

  • Guard latching at modal setupapplyMacReadability() is two-way now and runs from refreshMacList(), so a list that becomes readable again re-enables the controls instead of staying dead for the life of the modal. One residue inline: it moves the disabled flag in both directions but not the select's value, so a mode picked before the list went unreadable still rides along into doApply().
  • Whole form hostage to one sub-listapplyButton is out of the guarded set, and doApply() sends macModeChanged() ? macState() : '', which macfilter_want()`` reads as "leave the mode as it is" — so the unreadable-list path in settings_apply is only entered when the mode is actually being changed. Correct as far as it goes; the inline comment is about the one case where `macModeChanged()` is true and nobody meant it.
  • Hoisting Clear/Add — mechanical, and the guard reaching them is the point of the round. The per-address Remove buttons are deliberately not in the list, which is right: rebuildMacList() renders no rows at all when the list is unreadable, so there is nothing to gate.

Also checked: macClearButton still derives asked from s.macfilterstate rather than the select, which is correct — macfilter_clear judges the hazard against the mode the daemon has now, not the one pending in the form; and refreshMacList() assigning undefined on a failed read still reads as unreadable, since both macReadable() and rebuildMacList() test with ==/!= against null.

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 — the stale mode selection, and the translation template, which is the one artifact this round did not carry along.


Generated by Claude Code

Comment thread applications/luci-app-openthread/po/templates/openthread.pot Outdated
@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch from 2ae1f9d to 55b3186 Compare August 3, 2026 17:37
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
Gives the Thread border router a web interface: network status, the leader
and neighbour tables, scanning, joining and dataset configuration. It reaches
otbr-agent through an rpcd ucode backend rather than shelling out.

Carried here because it is still under review for the LuCI repository, as
openwrt/luci#8871, so it is not in the luci feed yet. Drop this copy once it
lands there.

Also drops the Lua LuCI app that openthread-br built as a second package of
its own. This replaces it, and two Makefiles defining luci-app-openthread made
the name ambiguous: kconfig reported it as a recursive dependency and gave up,
which left the package selection wrong for everything downstream of it.

The Makefile follows luci-app-easybird in including the luci feed's luci.mk
by absolute path, since this package lives outside that feed.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>
LorbusChris added a commit to LorbusChris/turris-os-packages that referenced this pull request Aug 3, 2026
openthread-br registers an "openthread" netifd protocol, but without a
LuCI protocol extension the interfaces page can only show "Unsupported
protocol type". Add the extension exposing the handler's options: the
backbone network, the radio URL, and optionally a startup dataset,
on-mesh prefixes and verbose logging.

Mirrors the package proposed for the LuCI repository alongside
luci-app-openthread (openwrt/luci#8871); drop this copy once it lands
there.

Assisted-By: Claude Opus 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 2ae1f9d (branch amended again into a single commit, 55b3186). Diff is a 10-line block in overview.js plus the regenerated template. Both threads from the last round are addressed:

  • Stale mode selection — taken as suggested: applyMacReadability() now reverts macSelect to s.macfilterstate || 'disable' when readability is lost while a pick is pending, so macModeChanged() goes false, doApply() sends '' and the rest of the form still applies. Keeping the applyMacVisibility() call is a defensible call — a select that disagrees with the rows beneath it is worse — with one residue inline: in the disable case that call also hides the "could not be read." box, which was the visible half of the explanation.
  • Translation template — regenerated, and it is a pure reference update: diff of the two .pot files with #: lines stripped is empty, so no msgid was added, removed or reworded. Checked the new references mechanically as well — all 198 #: entries resolve to a line that actually contains their msgid at 55b3186 (the 127 stale overview.js references from last round are gone).

Also checked, since the revert is new state-changing code on a hoisted path: applyMacVisibility() touches macRows, which is declared with var further down at line 570 — both call sites (line 560 via refreshMacList(), and line 665) run after the assignment, so the hoisting is safe. s.macfilterstate cannot be the daemon's literal "error" string here either, because macfilter_mode() maps that to null before settings_get returns it — which is what the second inline comment is about.

Two follow-ups inline. The first is the substantive one: the guard added last round keys on s.addrlist alone, while settings_apply refuses on an unreadable mode or an unreadable list, so the mirror-image partial read still reaches the whole-form refusal this round set out to prevent.


Generated by Claude Code

@LorbusChris
LorbusChris force-pushed the luci-app-openthread branch from 55b3186 to c1d1578 Compare August 5, 2026 16:13

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 55b3186 (branch amended again into a single commit, c1d1578). Diff is a ~20-line block in overview.js plus the regenerated template. All three threads from the last round are addressed:

  • Partial read of the snapshotmacReadable() now mirrors macfilter_snapshot()`` with s.addrlist != null && s.macfilterstate != null, so the mode-unreadable/list-readable half is covered too.
  • Per-address Remove buttons — gated as the follow-on asked, and collecting them fresh from macListBox on each pass is the right way round, since rebuildMacList() replaces those nodes on every refresh. querySelectorAll there can only match the Remove buttons: the box holds either the address rows or the single <em> error line.
  • Hint wording — "state" rather than "list", and the .pot carries exactly that one msgid rewording (a #:-stripped diff of the two templates shows nothing else changed).
  • applyMacVisibility() — keying the collapse on s.addrlist != null as well is correct and cannot leak addresses under a working disable select, since rows only exist when the list is readable.

Checked the regenerated template mechanically: all 198 #: references resolve to a line that actually contains their msgid at c1d1578, one msgid reworded, none added or removed.

CI on this head: the three FormalityCheck runs pass; the build workflow is still awaiting maintainer approval, so nothing to ground against there.

Three follow-ups inline. The first is the substantive one: refreshMacList() copies only addrlist out of the settings_get reply, so the s.macfilterstate term the guard just gained is frozen at modal-open time — which puts the mid-modal partial read back on the whole-form unreadable refusal this round set out to prevent.


Generated by Claude Code

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 86fa26d (branch amended again into a single commit, 8d40ca1). Diff is overview.js, the ucode backend and the regenerated template. Both threads from the last round are addressed:

  • Unknown filter mode — the select now carries a hidden, disabled Unknown placeholder and macSelect.value falls to '' whenever macfilterstate is null, so it no longer asserts Disable for a mode nobody could read; the Protocol row gets its own visible note in the same shape as the list's. macModeChanged() treating a null baseline as "no change" is what keeps doApply() sending an empty mode in that state, and settings_apply then never reaches its unreadable path — checked that the '' value is unreachable from macState() too, since the placeholder is disabled and the select is only ever assigned a real mode.
  • Clear's asked — now s.macfilterstate == 'allowlist' && (s.addrlist || []).length > 0, matching macfilter_clear's own refusal condition`` term for term, and refreshMacList() keeps both halves live so the claim stays accurate.
  • Third warning arm — the disable transition now says the filter stops applying rather than that the entries become blocked, and it still confirms, so macfilter_confirmed arrives true for the leftovers hazard the backend computes.
  • Readability gate — hoisting Clear/Add into variables does reach them; the one hole is the click-handler interaction noted inline.
  • Age column — 8 header cells, 8 body cells on both the neighbour and the joiner rows, and the placeholder colspan bumped to match.

Checked the regenerated openthread.pot mechanically: all 200 #: references resolve to a line that actually contains their msgid at 8d40ca1 (the only two mismatches my checker reported are its own escaping of and \'), and a reverse pass over the 149 _() literals in overview.js/errors.js finds every one of them present as a msgid — including the new Unknown, The filter mode could not be read., Age, Time since this device was last heard from and the reworded mode-switch sentence.

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. The first is a question about how far last round's empties_allowlist removal reaches; the second is a concrete one — ui.createHandlerFn re-enables the clicked button after applyMacReadability() has disabled it, so Add/Clear escape the very gate this round widened.


Generated by Claude Code

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 8d40ca1 (branch amended again into a single commit, 879e162). Diff is overview.js, the ucode backend and the regenerated template. Both threads from the last round are answered:

  • Stale-enabled Add/Clear — handled from both ends: each of the three handlers now returns early on !macReadable(), so a button that ui.createHandlerFn re-enabled cannot act, and refreshMacList() re-asserts the verdict from a window.setTimeout(…, 0). The ordering holds — the .finally() that does t.disabled = false runs as a microtask when the handler's promise settles, and the timeout is a macrotask, so it lands behind it. Nodes may be detached by then if the modal was closed, which is harmless. window.setTimeout is the in-tree spelling (23 uses).
  • Mode activation riding along on an add — answered by giving macfilter_add/macfilter_remove the same confirmed argument macfilter_clear and settings_apply take, placed after the leftovers / empties_allowlist hazards so the more specific reason still wins. The refusal is surfaced through the existing filterRefusals map. The frontend half of it is what my first inline comment is about: no caller passes confirmed, and the one UI path that switches a mode on is now a dead end.

openthread.pot checked mechanically: all 201 #: references resolve to a line that actually contains their msgid at 879e162 (the two my checker reports are its own handling of and \'), the reverse pass over the 150 _() literals in overview.js/errors.js finds every one present as a msgid, and exactly one msgid was added (Nothing was changed: the request would also switch the filter mode on…) with none removed.

CI on this head: the three FormalityCheck runs pass; the build workflow is still awaiting maintainer approval, so nothing to ground against there.

Commit checks

  • 879e162 "luci-app-openthread: add new application" — the feature list still ends with "and a network topology list view", but the restructure that dropped view.js left no topology view: the leader and neighbour tables are sections of the single overview page now, which the earlier "network overview with a live neighbor table" already covers. Same sentence in the PR body ("and a topology list view"). Everything else in the message matches what the diff adds.

Generated by Claude Code

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 86fa26d (branch amended again into a single commit, ff20643). Diff is overview.js, the ucode backend and the regenerated template. Both threads from the last round are addressed:

  • Modal snapshot driftrefreshMacList() now writes back both halves (s.addrlist and s.macfilterstate), so the mode-switch warning and the readability gate are sized from what the router reports now, not from what it reported when the modal opened. The Unknown placeholder option is the right shape for the select: with the mode unreadable it stops asserting Disable as a fact, and macModeNote says why on screen rather than in a tooltip on a disabled control.
  • Hazard question asked against stale statemacModeChanged() returns false against a null baseline, so doApply() sends an empty filter mode and settings_apply takes the "not being changed" path instead of refusing the whole form over a list nothing on it touched. The Clear button now claims approval only for a hazard the operator was actually shown (callMacfilterClear(asked)), which keeps the backend's own refusal live for one that appeared since.
  • The window.setTimeout(applyMacReadability, 0) reasoning holds: ui.createHandlerFn re-enables its own button from a .then() on the returned promise (microtask), and the timeout is a macrotask, so the re-apply really does land after the re-enable.
  • The third mode-switch outcome (→ disable) is now named in the warning instead of falling through to "they become blocked devices".

Also checked:

  • New Age column — verified against the daemon rather than assumed: otbr-agent emits Age in both the neighbor and the parent handler (blobmsg_add_string(&mBuf, "Age", transfer) in ot-br-posix src/openwrt/ubus/otubus.cpp), and summarize_neighbors passes neighbour entries through verbatim, so the field reaches the view. It arrives as a %3d-padded string, which num() trims before parseInt, so the cell renders. Header count is 8 and the empty-table colspan was bumped 7→8 to match; the joiner rows got their - cell too.
  • Translationsopenthread.pot checked mechanically: all 201 #: references resolve to a line that actually contains their msgid at ff20643, and the six new strings (Unknown, the two "could not be read" notes, Age + its tooltip, the unconfirmed_mode refusal, the disable-mode warning fragment) are present with correct 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.

Three follow-ups inline. The first is the substantive one: the new unconfirmed_mode guard can never be satisfied, because confirmed is not in either rpc.declare param list and rpcd fills req.args only from what the caller sent. The visible effect is that the per-neighbour Block is a no-op whenever the MAC filter is currently disabled — the operator confirms in the modal and gets back "Nothing was changed… change the mode from the network settings".


Generated by Claude Code

Comment thread applications/luci-app-openthread/root/usr/share/rpcd/ucode/luci.openthread Outdated

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since ff20643 (branch amended again into a single commit, 51d8328). Small round: the two rpc.declare param lists, three call sites, the two args type hints and the regenerated template. Both threads from the last round are addressed:

  • confirmed never reaching the backendcallMacfilterAdd / callMacfilterRemove now declare the third param, and rpc.declare's array form maps it positionally (rpc.js:302-305), so handleBlock with the filter disabled is no longer a no-op. Checked the two settings-modal call sites that pass only two arguments: params.confirmed is undefined, JSON.stringify drops it, and both send state: '', so macfilter_want()`` returns null and the new guard is not reached — mode changes still happen only in Save & Apply. The Unblock path deliberately omits it, which is right: no modal, no acknowledgement to claim, and `want == snap.mode` there anyway.
  • args policy spellingmacfilter_clear and settings_apply now say true like the other two; type hint only, so no behaviour change.

openthread.pot checked mechanically: all 201 #: references resolve to a line that actually contains their msgid at 51d8328 (the two my checker flags are its own handling of and \'), and the diff is references-only — no msgid added, removed or reworded.

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 about what the confirmed: true now being sent actually acknowledges: the Block modal doesn't mention the mode activation it is standing in for, and the Allow modal sends the flag unconditionally, which makes the stale-poll allowlist-of-one reachable by click again.

Commit checks

  • 51d8328 "luci-app-openthread: add new application" — still ends the feature list with "and a network topology list view" (same sentence in the PR body). git grep -i topolog over the package is empty and the menu registers a single openthread/overview entry, so there is no topology view since the restructure; the leader and neighbour tables are sections of the overview page, which "network overview with a live neighbor table" already covers. Carried over from the 879e162 round.

Generated by Claude Code

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 86fa26d (branch amended again into a single commit, 3be1579, now rebased onto current master). Diff is overview.js, the ucode backend and the regenerated template. Both threads from the last round are addressed, and the fix went further than the threads asked:

  • Settings modal vs. its own editsrefreshMacList() now writes both halves of the snapshot back (s.addrlist and s.macfilterstate), so Clear/Add/Remove no longer leave doApply() sizing the mode-switch warning from a stale list. Clear also stopped claiming approval it never asked for: callMacfilterClear(asked) passes exactly the condition macfilter_clear refuses on (mode == 'allowlist' && length(list) > 0), so a hazard that appeared since the modal opened is still caught server-side.
  • Unreadable ≠ disabled — the new Unknown placeholder, macReadable() gating and applyMacReadability() mirror settings_apply's snapshot rule in the modal. Walked the four state combinations: with only the mode unreadable the select shows Unknown, macModeChanged() stays false and doApply() sends an empty mode; with only the list unreadable a pick made while things were still readable is reverted, and applyMacVisibility()'s extra s.addrlist != null term keeps macRows on screen so the "could not be read" box is the visible explanation rather than a tooltip on a disabled select. The setTimeout(applyMacReadability, 0) ordering holds — ui.createHandlerFn re-enables its button from a microtask, the timeout is a macrotask, so the verdict wins.
  • macfilter_confirmed is now derived, not hardcodedwarn != null matches macfilter_hazard() case for case: entries > 0 → leftovers (confirmed), empty list + allowlist → empties_allowlist (confirmed), empty list + denylist/disable → no hazard, no prompt, no claim. And macModeChanged() ? macState() : '' keeps an unchanged mode off the want != null path entirely, so the form is no longer refused as unreadable over a list it does not touch.
  • unconfirmed_mode — checked that the new backend guard cannot be defeated by an absent argument: rpcd's args values are type hints only, rpc_ucode_validate_call_args() builds req.args from the received blob and supplies no defaults, so macfilter_confirmed: falsetrue in settings_apply is a pure no-op and an omitted confirmed really does read as unconfirmed. Traced the four reachable per-device paths (handleAllow, handleAllowlistBlock, the inline Unblock, and handleBlock under an already-active denylist): all have want == snap.mode, so none of them trips the new guard, and only the disable→denylist Block — the one case where the modal actually spells the switch out — sends an acknowledgement.
  • Age column — header, both row shapes and the placeholder colspan all moved 7 → 8 together, and num(bss.Age) degrades to - where the reply has no such field.
  • Translationsopenthread.pot checked mechanically: all 202 #: references resolve to a line that actually contains their msgid at 3be1579, eight msgids added (Age, Unknown, the two read-failure hints, the router-wide denylist paragraph, unconfirmed_mode, the reworded mode-switch warning and its new disable branch) and one removed (the old "gives them the opposite meaning" wording).
  • 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, cosmetic.


Generated by Claude Code

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, and joiner
commissioning.

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.

Assisted-By: Claude Fable 5
Signed-off-by: Christian Glombek <c.glombek@cosa.systems>

@openwrt-ai openwrt-ai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the changes since 3be1579 (branch amended again into a single commit, 467a4bc). Smallest round so far: one line and one comment in overview.js, plus the regenerated template. The single follow-up from the last round is addressed:

  • activates ? true : undefined — collapsed to activates, so handleBlock now sends confirmed: false explicitly instead of dropping the key. Checked that this is genuinely a no-op rather than a behaviour change hiding in a cosmetic edit: macfilter_add`` tests !req.args.confirmed, which reads the same for an absent key and an explicit `false`, and rpcd's `rpc_ucode_validate_call_args()` only walks the attributes that were actually sent — it rejects a present argument whose blob type disagrees with the policy, and `false` matches the `confirmed: true` bool hint, so nothing new can be refused as `INVALID_ARGUMENT`. The three other call sites (`handleAllowlistBlock`, `handleAllow`, the settings modal's add/remove) are untouched and still omit the flag.
  • The reworded comment above it still describes what the code does — activates is neigh.macfilter == 'disable', which is exactly the case the modal spells the mode switch out for, and a row that went stale under the 2 s poll still lands on unconfirmed_mode server-side.

Also checked:

  • Translationsopenthread.pot is a references-only regeneration this round: a #:-stripped diff of the two templates is empty, so no msgid was added, removed or reworded, and the shifts are all the single-line deletion (overview.js:11241123, etc.). All 202 #: entries resolve to a line that actually contains their msgid at 467a4bc (the two my checker reports are its own handling of and \').
  • Commit message — the "and a network topology list view" clause I flagged in the 879e162 and 51d8328 rounds is gone from the message; the feature list now matches what the diff adds. git grep -i topolog over the package is still empty, so nothing else claims a view that does not exist.
  • CI on this head — no checks have reported yet (total_count: 0, pending), so there is nothing to ground against.

Reviewed 1 new commit; no new issues found.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

add package Introduces a new package Makefile build script

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants