Skip to content

Luci app qosify counters - #9051

Closed
choppyc79 wants to merge 5 commits into
openwrt:masterfrom
choppyc79:luci-app-qosify-counters
Closed

choppyc79 wants to merge 5 commits into
openwrt:masterfrom
choppyc79:luci-app-qosify-counters

Conversation

@choppyc79

@choppyc79 choppyc79 commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Pull request details

Description

Depends on #9046, #9047, #9048, #9050. Only the last commit is this PR; draft until those are merged.

New Counters tab, polled at LuCI's interval and only while it is open:

  • Traffic by Class: ubus call qosify get_stats per class (dscp, packets,
    bytes, share, total), bars cube-root scaled, grouped and coloured by the
    CAKE tin the class's egress codepoint lands in
  • Traffic by CAKE Tin: the qosify-status tc output summed tin by tin across
    egress and ingress (pkts, bytes, drops; marks on hover)
  • DNS Entries: ubus call qosify dump dns entries with hits/packets/bytes from
    the get_stats dns table

Works with both get_stats shapes: master (beeb87e) and 1501e09 as shipped
in 24.10/25.12, which has per-class packets only and no dns table. There,
the daemon-info and DNS boxes stay hidden and dump is not called.
Everything is cleared while qosify is stopped.

ACL: read group gains qosify get_stats and dump. Traffic by CAKE Tin uses
the existing qosify-status exec grant, so a read-only session does not
see it.

Screenshot or video of changes

Maintainer

@choppyc79


Tested on

OpenWrt version: SNAPSHOT r36237-2ed2c210c3
LuCI version: 26.257.37274~f4eebd4
Web browser(s): Google Chrome


Checklist

  • (Nice to have) Includes what Issue it closes (e.g. openwrt/luci#issue-number).
  • (Nice to have) Includes what it depends on (e.g. openwrt/packages#pr-number in sister repo).
luci-app-qosify Counters 2 luci-app-qosify Counters 1

Comment thread applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js Outdated
Comment thread applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js Outdated
Comment thread applications/luci-app-qosify/htdocs/luci-static/resources/view/qosify/main.js Outdated
@choppyc79
choppyc79 force-pushed the luci-app-qosify-counters branch 2 times, most recently from a4a6b40 to 0b4459e Compare September 20, 2026 10:50

@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.

Commit checks

  • 7253035 "report an unanswered status call as unknown" — body says the start, stop, restart and reload buttons "report the failing call's own error", but they now throw a synthesized "rpcd is not answering ... service state is unknown" when waitForState() returns null. The waitForRunning/waitForStoppedwaitForState unification and the new applyService abort are also undescribed; mention both, or drop the "own error" sentence.
  • aeff225 "build the page from stock LuCI markup" — body says "No behaviour or string changes", but the diff adds a No classes defined in %s empty-state row to the Config reference class box (qos-cfg-cls), which had none before. Either note that empty state in the body or move it to its own commit.

Generated by Claude Code

applyService:function(){
var self=this;
return L.resolveDefault(callServiceList('qosify'),{}).then(function(r){
return callServiceList('qosify').catch(function(){throw new Error(_('rpcd is not answering for qosify, so the service state is unknown.'));}).then(function(r){

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.

An unanswered service.list now aborts before any rc.init call, so a config writeConfig()`` has already written to disk is left unapplied and reported as Save failed. The stale-ACL case this commit's message names grants `rc` but not `service`, and there the old path still reached `rc.init start`. Better to fall through to `rc.init` with the running state unknown than to throw before trying.


Generated by Claude Code

Comment on lines +652 to +654
if(ctx.active==null&&enChecked){el.className='label warning';dom.content(el,_('Status Unknown'));}
else if(ctx.active){el.className='label success';dom.content(el,_('Active'));}
else if(ctx.running&&enChecked){el.className='label warning';dom.content(el,_('Enabled — Not Shaping (check config)'));}

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.

ctx.running===null with ctx.active===falseservice.list unanswered while qosify.status answered, which gatherCtx() now produces since the two facts go null independently — falls through to Enabled — Not Running. That contradicts the Unknown svcNodes()`` puts on the Running row from the same ctx.

Suggested change
if(ctx.active==null&&enChecked){el.className='label warning';dom.content(el,_('Status Unknown'));}
else if(ctx.active){el.className='label success';dom.content(el,_('Active'));}
else if(ctx.running&&enChecked){el.className='label warning';dom.content(el,_('Enabled — Not Shaping (check config)'));}
if(ctx.active==null&&enChecked){el.className='label warning';dom.content(el,_('Status Unknown'));}
else if(ctx.active){el.className='label success';dom.content(el,_('Active'));}
else if(ctx.running==null&&enChecked){el.className='label warning';dom.content(el,_('Status Unknown'));}
else if(ctx.running&&enChecked){el.className='label warning';dom.content(el,_('Enabled — Not Shaping (check config)'));}

Generated by Claude Code

@choppyc79
choppyc79 force-pushed the luci-app-qosify-counters branch from 0b4459e to 8d0a3e5 Compare September 20, 2026 18:36

@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 5 new commits; no new issues found.


Generated by Claude Code

rpc.declare() resolves a ubus error status unless reject is set, so a
status code (6 once the session's ACL no longer covers the object, 4
when the object is gone) stays in result[0] and expect {'': {}}
rewrites it to the same {} a working call with nothing to report
returns. L.resolveDefault() cannot tell the two apart, so an ACL left
over from an older install, a stale session or a stopped rpcd reads as
a stopped, unshaped qosify with no init script.

Declare rc.list, service.list and qosify.status with reject: true and
catch each rejection to null. running, enabled, hasInit, active and
shaped are then null rather than false when the call was not answered,
and the Overview service table shows an amber Unknown badge for each,
with the cause named once, on the first row that reads Unknown. The
qosify object goes with the daemon, so an unanswered status call while
qosify is stopped still reads as not shaping.

The autostart button carries the state as its label, so it shows
Unknown and is disabled while the state is unknown. Its click now reads
the state from the last refresh rather than from when the button was
built, so a tick that changes the state also changes the action. The
start, stop, restart and reload buttons stay clickable. When the
service.list poll after a start, restart or stop goes unanswered, the
action fails with "rpcd is not answering" rather than reporting a
result it could not check, and a stop then skips
/usr/share/qosify-luci/cleanup instead of cleaning up.

A save no longer warns that qosify is not shaping when the check could
not run; it says shaping could not be checked. When service.list goes
unanswered while a save applies, qosify is restarted rather than
reloaded or started, since the config is already written and the
running state is unknown. That restart also starts a qosify that was
stopped.

The Status tab says rpcd is not answering rather than that qosify is
not running, and says the interface summary is unavailable when only
qosify status went unanswered.

updateEnBadge() gives the Config tab's QoS Enabled badge the same
treatment: with the section enabled and the status call unanswered it
reads Status Unknown, rather than the Enabled — Not Shaping (check
config) or Enabled — Not Running a null active used to fall through to.
A section disabled in UCI still reads Disabled — that state comes from
the config, not from the call, so an unanswered call does not put it in
doubt.

po/templates/qosify.pot regenerated with build/i18n-scan.pl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ash Clarke <clarkeaj@hotmail.co.uk>
@choppyc79
choppyc79 force-pushed the luci-app-qosify-counters branch from 8d0a3e5 to 721d151 Compare September 21, 2026 06:49
choppyc79 and others added 4 commits September 21, 2026 07:58
A rules edit only needs the mapping files re-read, but the only way to
get that from the page is Reload, the init script's reload_service(),
which pushes the whole config. Add Reload Rules next to it: ubus call
qosify reload runs qosify_map_reload(), which re-reads the files in the
defaults list and leaves the qdiscs, the interface config and the
dynamically added entries alone.

Add Check Devices to the Advanced tab: ubus call qosify check_devices
runs qosify_iface_check(), which starts a section whose device now
exists and stops one whose device has gone, so a device that appears
after qosify started is picked up without a restart rebuilding every
qdisc. The method arms a 10 ms uloop timer and returns before the pass
runs, so the page waits before refreshing the service state.

Both methods return an empty reply, so they are declared with
reject: true; without it a failed call would report success. Both are
in qosify 1501e09 (24.10, 25.12) and master. Grant them in the write
ACL group.

po/templates/qosify.pot regenerated with build/i18n-scan.pl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ash Clarke <clarkeaj@hotmail.co.uk>
Both pollers pass a fixed 10 s to poll.add(), so the page ignores
luci.main.pollinterval. Leave the interval out so poll.add() takes
L.env.pollinterval (5 s unless set), which the user can change.

On the default setting the Status tab now forks qosify-status every
5 s rather than 10 s. It still does so only while that tab is open, and
Poll.step() still holds the next tick until the last one settles, so a
fork slower than the interval skips ticks instead of stacking up. A
target that cannot afford the fork at that rate raises
luci.main.pollinterval, which slows the rest of LuCI's polling with it,
instead of the page choosing its own rate.

Pausing with the header refresh toggle is unchanged, as it stops the
whole poll loop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ash Clarke <clarkeaj@hotmail.co.uk>
The page draws its own badges, key/value tables, reference boxes and
muted notes with qos-* classes and inline styles, inside fieldset and
legend sections, so it looks the same under every theme and ignores the
theme's own table, label and section styles. Use the markup the themes
already style instead:

- sections are div.cbi-section with an h3 title
- status and file validity use .label success, warning and danger
- key/value tables are .table with .tr and .td rows
- Quick Settings and the Advanced file rows are .cbi-value rows, with
  hints as .cbi-value-description
- notes are cbi-section-descr, <p> or <em>, and code is <code>

qosify.css now draws each section as a box with a title bar from the
theme's own variables, gives the two editors and the tc output their
size, and lays out the Quick Add rows. Bootstrap and OpenWrt 2020 have
no .label.danger, so the sheet supplies it from --error-color-high.

The editor width moves from the qos-edit class to the textarea ids with
box-sizing: border-box, next to the height, and the editors resize in
both directions, so the theme's textarea width cannot win and a wrong
width stays correctable.

The Config Reference class list becomes a .table like the one on the
Rules tab, so it now picks up that tab's existing "No classes defined
in %s" row when no class or alias is configured, where the plain div
rendered nothing. That is the only behaviour change, and no string is
added or altered.

po/templates/qosify.pot regenerated with build/i18n-scan.pl for the
moved source lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ash Clarke <clarkeaj@hotmail.co.uk>
qosify keeps per-class counters and, on master, per-DNS-pattern and
eBPF map statistics, but the page shows none of them. Add a Counters
tab, polled like the others and only while it is open.

Traffic by Class lists the classes from ubus call qosify get_stats with
their dscp, packets, bytes and share, and a total row. Bar length is
the cube root of each row against the largest, so one bulk flow does
not flatten every other row; the share column stays exact. Rows are
grouped and coloured by the CAKE tin their egress codepoint lands in
under the configured diffserv mode, and AFx1 to AFx3 sort by drop
precedence. The tcp_default and udp_default slots are skipped, as a
dscp_default_* naming a class is already counted against that class.

get_stats has two shapes. qosify master (beeb87e) returns a classes
table plus ebpf_map_entries, last_reload_time, dns_cache, dscp and dns;
1501e09 (24.10, 25.12) returns qosify_map_stats() at the top level, one
table per class with packets only. Both are read, and only the fields
the reply carries are shown.

Traffic by CAKE Tin graphs the qosify-status output tin by tin, egress
and ingress summed from the pkts, bytes and drops rows tc prints, with
marks on hover. Qdiscs running a different diffserv mode get a table of
their own. It needs the existing qosify-status exec grant, so a
read-only session does not get it.

DNS Entries lists the dns entries from ubus call qosify dump with hits,
packets and bytes from the get_stats dns table. It is shown only when
get_stats carries that table, so dump is never called on 1501e09.

While qosify is stopped the figures are cleared rather than left on
screen looking live. get_stats and dump are declared with reject: true,
so a failed call is reported rather than read as an empty reply. Tin
colours use the theme's error, primary, warn and success colours, with
light and dark fallbacks for the extra diffserv8 and precedence tins.

Grant get_stats and dump in the read ACL group.

po/templates/qosify.pot regenerated with build/i18n-scan.pl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Ash Clarke <clarkeaj@hotmail.co.uk>
@choppyc79
choppyc79 force-pushed the luci-app-qosify-counters branch from 721d151 to 433ce13 Compare September 21, 2026 07:03
@choppyc79

Copy link
Copy Markdown
Contributor Author

Folded into #9054, which carries this commit unchanged as part of the
series. Closing in its favour.

@choppyc79 choppyc79 closed this Sep 21, 2026
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.

2 participants