' +
206 | '
' + _('Example rules:') + '
' +
207 | '
' +
208 | 'chain ingress {\n' +
209 | ' type filter hook ingress device eth1 priority -500; policy accept;\n' +
210 | ' iif eth1 counter ip dscp set cs0 comment "Wash all ISP DSCP marks to CS0 (IPv4)"\n' +
211 | ' iif eth1 counter ip6 dscp set cs0 comment "Wash all ISP DSCP marks to CS0 (IPv6)"\n' +
212 | '}\n\n' +
213 | 'chain forward {\n' +
214 | ' type filter hook forward priority 0; policy accept;\n' +
215 | ' # Limit and mark high-rate TCP traffic from specific IP\n' +
216 | ' ip saddr 192.168.138.100 tcp flags & (fin|syn|rst|ack) != 0\n' +
217 | ' limit rate over 300/second burst 300 packets\n' +
218 | ' counter ip dscp set cs1\n' +
219 | ' comment "Mark TCP traffic from 192.168.138.100 exceeding 300 pps as CS1"\n' +
220 | '}\n' +
221 | '
' +
222 | '
' + _('Warning:') + ' ' + _('Incorrect rules can disrupt network functionality. Use with caution.') + '
' +
223 | '
';
224 |
225 | return m.render();
226 | }
227 | });
228 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/view/hfsc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | 'require view';
3 | 'require form';
4 | 'require ui';
5 | 'require uci';
6 | 'require rpc';
7 | 'require fs';
8 |
9 | var callInitAction = rpc.declare({
10 | object: 'luci',
11 | method: 'setInitAction',
12 | params: ['name', 'action'],
13 | expect: { result: false }
14 | });
15 |
16 | return view.extend({
17 | handleSaveApply: function(ev) {
18 | return this.handleSave(ev)
19 | .then(() => ui.changes.apply())
20 | .then(() => uci.load('qosmate'))
21 | .then(() => uci.get_first('qosmate', 'global', 'enabled'))
22 | .then(enabled => {
23 | if (enabled === '0') {
24 | return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
25 | } else {
26 | return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
27 | }
28 | })
29 | .then(() => {
30 | ui.hideModal();
31 | window.location.reload();
32 | })
33 | .catch(err => {
34 | ui.hideModal();
35 | ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
36 | });
37 | },
38 |
39 | render: function() {
40 | var m, s, o;
41 |
42 | m = new form.Map('qosmate', _('QoSmate HFSC Settings'), _('Configure HFSC settings for QoSmate.'));
43 |
44 | s = m.section(form.NamedSection, 'hfsc', 'hfsc', _('HFSC Settings'));
45 | s.anonymous = true;
46 |
47 | function createOption(name, title, description, placeholder, datatype) {
48 | var opt = s.option(form.Value, name, title, description);
49 | opt.datatype = datatype || 'string';
50 | opt.rmempty = true;
51 | opt.placeholder = placeholder;
52 |
53 | if (datatype === 'uinteger') {
54 | opt.validate = function(section_id, value) {
55 | if (value === '' || value === null) return true;
56 | if (!/^\d+$/.test(value)) return _('Must be a non-negative integer or empty');
57 | return true;
58 | };
59 | }
60 | return opt;
61 | }
62 |
63 | o = s.option(form.ListValue, 'LINKTYPE', _('Link Type'), _('Select the link type'));
64 | o.value('ethernet', _('Ethernet'));
65 | o.value('atm', _('ATM'));
66 | o.value('adsl', _('ADSL'));
67 | o.default = 'ethernet';
68 |
69 | createOption('OH', _('Overhead'), _('Set the overhead'), _('Default: 44'), 'uinteger');
70 |
71 | o = s.option(form.ListValue, 'gameqdisc', _('Game Queue Discipline'), _('Queueing method for traffic classified as realtime'));
72 | o.value('pfifo', _('PFIFO'));
73 | o.value('fq_codel', _('FQ_CODEL'));
74 | o.value('bfifo', _('BFIFO'));
75 | o.value('red', _('RED'));
76 | o.value('netem', _('NETEM'));
77 | o.default = 'pfifo';
78 |
79 | createOption('GAMEUP', _('Game Upload (kbps)'), _('Bandwidth reserved for realtime upload traffic'), _('Default: 15% of UPRATE + 400'), 'uinteger');
80 | createOption('GAMEDOWN', _('Game Download (kbps)'), _('Bandwidth reserved for realtime download traffic'), _('Default: 15% of DOWNRATE + 400'), 'uinteger');
81 |
82 | o = s.option(form.ListValue, 'nongameqdisc', _('Non-Game Queue Discipline'), _('Select the queueing discipline for non-realtime traffic'));
83 | o.value('fq_codel', _('FQ_CODEL'));
84 | o.value('cake', _('CAKE'));
85 | o.default = 'fq_codel';
86 |
87 | createOption('nongameqdiscoptions', _('Non-Game QDisc Options'), _('Cake options for non-realtime queueing discipline'), _('Default: besteffort ack-filter'));
88 | createOption('MAXDEL', _('Max Delay (ms)'), _('Target max delay for realtime packets after burst (pfifo, bfifo, red)'), _('Default: 24'), 'uinteger');
89 | createOption('PFIFOMIN', _('PFIFO Min'), _('Minimum packet count for PFIFO queue'), _('Default: 5'), 'uinteger');
90 | createOption('PACKETSIZE', _('Avg Packet Size (B)'), _('Used with PFIFOMIN to calculate PFIFO limit'), _('Default: 450'), 'uinteger');
91 | createOption('netemdelayms', _('NETEM Delay (ms)'), _('NETEM delay in milliseconds'), _('Default: 30'), 'uinteger');
92 | createOption('netemjitterms', _('NETEM Jitter (ms)'), _('NETEM jitter in milliseconds'), _('Default: 7'), 'uinteger');
93 |
94 | o = s.option(form.ListValue, 'netem_direction', _('NETEM Direction'), _('Select which direction to apply the NETEM delay/jitter settings'));
95 | o.depends('gameqdisc', 'netem');
96 | o.value('both', _('Both Directions'));
97 | o.value('egress', _('Egress Only'));
98 | o.value('ingress', _('Ingress Only'));
99 | o.default = 'both';
100 |
101 | o = s.option(form.ListValue, 'netemdist', _('NETEM Distribution'), _('NETEM delay distribution'));
102 | o.value('experimental', _('Experimental'));
103 | o.value('normal', _('Normal'));
104 | o.value('pareto', _('Pareto'));
105 | o.value('paretonormal', _('Pareto Normal'));
106 | o.default = 'normal';
107 |
108 | createOption('pktlossp', _('Packet Loss Percentage'), _('Percentage of packet loss'), _('Default: none'));
109 |
110 | return m.render();
111 | }
112 | });
113 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/view/ipsets.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | 'require view';
3 | 'require rpc';
4 | 'require ui';
5 | 'require uci';
6 | 'require form';
7 | 'require fs';
8 | 'require tools.widgets as widgets';
9 |
10 | var callInitAction = rpc.declare({
11 | object: 'luci',
12 | method: 'setInitAction',
13 | params: ['name', 'action'],
14 | expect: { result: false }
15 | });
16 |
17 | return view.extend({
18 | handleSaveApply: function(ev) {
19 | return this.handleSave(ev)
20 | .then(() => ui.changes.apply())
21 | .then(() => uci.load('qosmate'))
22 | .then(() => uci.get_first('qosmate', 'global', 'enabled'))
23 | .then(enabled => {
24 | if (enabled === '0') {
25 | return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
26 | } else {
27 | return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
28 | }
29 | })
30 | .then(() => {
31 | ui.hideModal();
32 | window.location.reload();
33 | })
34 | .catch(err => {
35 | ui.hideModal();
36 | ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
37 | });
38 | },
39 |
40 | render: function() {
41 | var m, s, o;
42 |
43 | m = new form.Map('qosmate', _('QoSmate IP Sets'),
44 | _('Define groups of IP addresses that can be referenced in QoS rules using @setname'));
45 |
46 | s = m.section(form.GridSection, 'ipset', _('IP Sets'));
47 | s.addremove = true;
48 | s.anonymous = true;
49 | s.sortable = true;
50 |
51 | o = s.option(form.Value, 'name', _('Set Name'));
52 | o.rmempty = false;
53 | o.validate = function(section_id, value) {
54 | if (!value) {
55 | return _('Set name is required');
56 | }
57 | if (!/^[a-zA-Z0-9_]+$/.test(value)) {
58 | return _('Set name must contain only letters, numbers, and underscore');
59 | }
60 | return true;
61 | };
62 |
63 | o = s.option(form.ListValue, 'mode', _('Set Mode'));
64 | o.value('static', _('Static'));
65 | o.value('dynamic', _('Dynamic'));
66 | o.default = 'static';
67 | o.rmempty = false;
68 |
69 | o = s.option(form.ListValue, 'family', _('Family'));
70 | o.value('ipv4', _('IPv4'));
71 | o.value('ipv6', _('IPv6'));
72 | o.default = 'ipv4';
73 | o.rmempty = false;
74 |
75 | o = s.option(form.DynamicList, 'ip4', _('IPv4 Addresses'));
76 | o.datatype = 'ip4addr';
77 | o.rmempty = true;
78 | o.placeholder = _('Add IPv4 address or subnet');
79 | o.depends({ mode: 'static', family: 'ipv4' });
80 |
81 | o = s.option(form.DynamicList, 'ip6', _('IPv6 Addresses'));
82 | o.datatype = 'ip6addr';
83 | o.rmempty = true;
84 | o.placeholder = _('Add IPv6 address or subnet');
85 | o.depends({ mode: 'static', family: 'ipv6' });
86 |
87 | o = s.option(form.Value, 'timeout', _('Timeout'));
88 | o.placeholder = _('e.g., 1h');
89 | o.depends('mode', 'dynamic');
90 | o.validate = function(section_id, value) {
91 | if (!value)
92 | return true; // Allow empty value if applicable
93 | // Validate that the timeout is in a valid format (e.g., combination of numbers and allowed time units: h, m, s)
94 | if (!/^(\d+[hms])+$/.test(value)) {
95 | return _('Timeout must be in a valid format (e.g., "10s", "1h", "2h12m10s")');
96 | }
97 | return true;
98 | };
99 |
100 | o = s.option(form.Flag, 'enabled', _('Enabled'));
101 | o.default = '1';
102 | o.rmempty = false;
103 |
104 | return m.render();
105 | }
106 | });
107 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/view/rules.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | 'require view';
3 | 'require ui';
4 | 'require uci';
5 | 'require form';
6 | 'require rpc';
7 | 'require fs';
8 | 'require tools.widgets as widgets';
9 |
10 | var callInitAction = rpc.declare({
11 | object: 'luci',
12 | method: 'setInitAction',
13 | params: ['name', 'action'],
14 | expect: { result: false }
15 | });
16 |
17 | return view.extend({
18 | handleSaveApply: function(ev) {
19 | return this.handleSave(ev)
20 | .then(() => ui.changes.apply())
21 | .then(() => uci.load('qosmate'))
22 | .then(() => uci.get_first('qosmate', 'global', 'enabled'))
23 | .then(enabled => {
24 | if (enabled === '0') {
25 | return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
26 | } else {
27 | return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
28 | }
29 | })
30 | .then(() => {
31 | ui.hideModal();
32 | window.location.reload();
33 | })
34 | .catch(err => {
35 | ui.hideModal();
36 | ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
37 | });
38 | },
39 |
40 | render: function() {
41 | var m, s, o;
42 |
43 | m = new form.Map('qosmate', _('QoSmate Rules'),
44 | _('Configure QoS rules for marking packets with DSCP values.'));
45 |
46 | s = m.section(form.GridSection, 'rule', _('Rules'));
47 | s.addremove = true;
48 | s.anonymous = true;
49 | s.sortable = true;
50 |
51 | s.tab('general', _('General Settings'));
52 | s.tab('mapping', _('DSCP Mapping'));
53 |
54 | // Add mapping information to the description
55 | s.description = E('div', { 'class': 'cbi-section-descr' }, [
56 | E('h4', _('HFSC Mapping:')),
57 | E('table', { 'class': 'table' }, [
58 | E('tr', { 'class': 'tr' }, [
59 | E('td', { 'class': 'td left', 'width': '50%' }, _('High Priority [Realtime] (1:11)')),
60 | E('td', { 'class': 'td left' }, 'EF, CS5, CS6, CS7')
61 | ]),
62 | E('tr', { 'class': 'tr' }, [
63 | E('td', { 'class': 'td left' }, _('Fast Non-Realtime (1:12)')),
64 | E('td', { 'class': 'td left' }, 'CS4, AF41, AF42')
65 | ]),
66 | E('tr', { 'class': 'tr' }, [
67 | E('td', { 'class': 'td left' }, _('Normal (1:13)')),
68 | E('td', { 'class': 'td left' }, 'CS0')
69 | ]),
70 | E('tr', { 'class': 'tr' }, [
71 | E('td', { 'class': 'td left' }, _('Low Priority (1:14)')),
72 | E('td', { 'class': 'td left' }, 'CS2, AF11')
73 | ]),
74 | E('tr', { 'class': 'tr' }, [
75 | E('td', { 'class': 'td left' }, _('Bulk (1:15)')),
76 | E('td', { 'class': 'td left' }, 'CS1')
77 | ])
78 | ]),
79 | E('h4', _('CAKE Mapping (diffserv4):')),
80 | E('table', { 'class': 'table' }, [
81 | E('tr', { 'class': 'tr' }, [
82 | E('td', { 'class': 'td left', 'width': '50%' }, _('Voice (Highest Priority)')),
83 | E('td', { 'class': 'td left' }, 'CS7, CS6, EF, VA, CS5, CS4')
84 | ]),
85 | E('tr', { 'class': 'tr' }, [
86 | E('td', { 'class': 'td left' }, _('Video')),
87 | E('td', { 'class': 'td left' }, 'CS3, AF4x, AF3x, AF2x, CS2, TOS1')
88 | ]),
89 | E('tr', { 'class': 'tr' }, [
90 | E('td', { 'class': 'td left' }, _('Best Effort')),
91 | E('td', { 'class': 'td left' }, 'CS0, AF1x, TOS0')
92 | ]),
93 | E('tr', { 'class': 'tr' }, [
94 | E('td', { 'class': 'td left' }, _('Bulk (Lowest Priority)')),
95 | E('td', { 'class': 'td left' }, 'CS1, LE')
96 | ])
97 | ])
98 | ]);
99 |
100 | o = s.taboption('general', form.Value, 'name', _('Name'));
101 | o.rmempty = false;
102 |
103 | o = s.option(form.DummyValue, 'proto', _('Protocol'));
104 | o.cfgvalue = function(section_id) {
105 | var proto = uci.get('qosmate', section_id, 'proto');
106 | if (Array.isArray(proto)) {
107 | return proto.map(function(p) { return p.toUpperCase(); }).join(', ');
108 | } else if (typeof proto === 'string') {
109 | return proto.toUpperCase();
110 | }
111 | return _('Any');
112 | };
113 |
114 | o = s.taboption('general', form.MultiValue, 'proto', _('Protocol'));
115 | o.value('tcp', _('TCP'));
116 | o.value('udp', _('UDP'));
117 | o.value('icmp', _('ICMP'));
118 | o.rmempty = true;
119 | o.default = 'tcp udp';
120 | o.modalonly = true;
121 |
122 | o.cfgvalue = function(section_id) {
123 | var value = uci.get('qosmate', section_id, 'proto');
124 | if (Array.isArray(value)) {
125 | return value;
126 | } else if (typeof value === 'string') {
127 | return value.split(/\s+/);
128 | }
129 | return [];
130 | };
131 |
132 | o.write = function(section_id, value) {
133 | if (value && value.length) {
134 | uci.set('qosmate', section_id, 'proto', value.join(' '));
135 | } else {
136 | uci.unset('qosmate', section_id, 'proto');
137 | }
138 | };
139 |
140 | o.validate = function(section_id, value) {
141 | if (!value || value.length === 0) {
142 | return true;
143 | }
144 |
145 | var valid = ['tcp', 'udp', 'icmp'];
146 | var toValidate = Array.isArray(value) ? value : value.split(/\s+/);
147 |
148 | for (var i = 0; i < toValidate.length; i++) {
149 | if (valid.indexOf(toValidate[i]) === -1) {
150 | return _('Invalid protocol: %s').format(toValidate[i]);
151 | }
152 | }
153 | return true;
154 | };
155 |
156 | o.remove = function(section_id) {
157 | uci.unset('qosmate', section_id, 'proto');
158 | };
159 |
160 | o = s.taboption('general', form.DynamicList, 'src_ip', _('Source IP'));
161 | o.datatype = 'string';
162 | o.placeholder = _('IP address or @setname');
163 | o.rmempty = true;
164 | o.validate = function(section_id, value) {
165 | if (!value || value.length === 0) {
166 | return true;
167 | }
168 |
169 | var values = Array.isArray(value) ? value : value.split(/\s+/);
170 | var ipCidrRegex = /^(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3})(?:\/(?:[0-9]|[1-2]\d|3[0-2]))?|(?:(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}|(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}|(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}|(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}|[A-Fa-f0-9]{1,4}:(?:(?::[A-Fa-f0-9]{1,4}){1,6})|:(?:(?::[A-Fa-f0-9]{1,4}){1,7}|:))(?:\/(?:[0-9]|[1-9]\d|1[0-1]\d|12[0-8]))?)$/;
171 |
172 | for (var i = 0; i < values.length; i++) {
173 | var v = values[i].replace(/^!(?!=)/, '!=');
174 | if (v.startsWith('@')) {
175 | if (!/^@[a-zA-Z0-9_]+$/.test(v)) {
176 | return _('Invalid set name format. Must start with @ followed by letters, numbers, or underscore');
177 | }
178 | } else {
179 | if (!ipCidrRegex.test(v)) {
180 | return _('Invalid IP address or CIDR format: ') + v;
181 | }
182 | }
183 | }
184 | return true;
185 | };
186 | o.write = function(section_id, formvalue) {
187 | var values = formvalue.map(function(v) {
188 | return v.replace(/^!(?!=)/, '!=');
189 | });
190 | return this.super('write', [section_id, values]);
191 | };
192 |
193 | o = s.taboption('general', form.DynamicList, 'src_port', _('Source port'));
194 | o.datatype = 'list(neg(portrange))';
195 | o.placeholder = _('any');
196 | o.rmempty = true;
197 | o.write = function(section_id, formvalue) {
198 | var values = formvalue.map(function(v) {
199 | return v.replace(/^!(?!=)/, '!=');
200 | });
201 | return this.super('write', [section_id, values]);
202 | };
203 |
204 | o = s.taboption('general', form.DynamicList, 'dest_ip', _('Destination IP'));
205 | o.datatype = 'string';
206 | o.placeholder = _('IP address or @setname');
207 | o.rmempty = true;
208 | o.validate = function(section_id, value) {
209 | if (!value || value.length === 0) {
210 | return true;
211 | }
212 |
213 | var values = Array.isArray(value) ? value : value.split(/\s+/);
214 | var ipCidrRegex = /^(?:(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)(?:\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)){3})(?:\/(?:[0-9]|[1-2]\d|3[0-2]))?|(?:(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,7}:|(?:[A-Fa-f0-9]{1,4}:){1,6}:[A-Fa-f0-9]{1,4}|(?:[A-Fa-f0-9]{1,4}:){1,5}(?::[A-Fa-f0-9]{1,4}){1,2}|(?:[A-Fa-f0-9]{1,4}:){1,4}(?::[A-Fa-f0-9]{1,4}){1,3}|(?:[A-Fa-f0-9]{1,4}:){1,3}(?::[A-Fa-f0-9]{1,4}){1,4}|(?:[A-Fa-f0-9]{1,4}:){1,2}(?::[A-Fa-f0-9]{1,4}){1,5}|[A-Fa-f0-9]{1,4}:(?:(?::[A-Fa-f0-9]{1,4}){1,6})|:(?:(?::[A-Fa-f0-9]{1,4}){1,7}|:))(?:\/(?:[0-9]|[1-9]\d|1[0-1]\d|12[0-8]))?)$/;
215 |
216 | for (var i = 0; i < values.length; i++) {
217 | var v = values[i].replace(/^!(?!=)/, '!=');
218 | if (v.startsWith('@')) {
219 | if (!/^@[a-zA-Z0-9_]+$/.test(v)) {
220 | return _('Invalid set name format. Must start with @ followed by letters, numbers, or underscore');
221 | }
222 | } else {
223 | if (!ipCidrRegex.test(v)) {
224 | return _('Invalid IP address or CIDR format: ') + v;
225 | }
226 | }
227 | }
228 | return true;
229 | };
230 | o.write = function(section_id, formvalue) {
231 | var values = formvalue.map(function(v) {
232 | return v.replace(/^!(?!=)/, '!=');
233 | });
234 | return this.super('write', [section_id, values]);
235 | };
236 |
237 | o = s.taboption('general', form.DynamicList, 'dest_port', _('Destination port'));
238 | o.datatype = 'list(neg(portrange))';
239 | o.placeholder = _('any');
240 | o.rmempty = true;
241 | o.write = function(section_id, formvalue) {
242 | var values = formvalue.map(function(v) {
243 | return v.replace(/^!(?!=)/, '!=');
244 | });
245 | return this.super('write', [section_id, values]);
246 | };
247 |
248 | o = s.taboption('general', form.ListValue, 'class', _('DSCP Class'));
249 | o.value('ef', _('EF - Expedited Forwarding (46)'));
250 | o.value('cs5', _('CS5 (40)'));
251 | o.value('cs6', _('CS6 (48)'));
252 | o.value('cs7', _('CS7 (56)'));
253 | o.value('cs4', _('CS4 (32)'));
254 | o.value('af41', _('AF41 (34)'));
255 | o.value('af42', _('AF42 (36)'));
256 | o.value('af11', _('AF11 (10)'));
257 | o.value('cs2', _('CS2 (16)'));
258 | o.value('cs1', _('CS1 (8)'));
259 | o.value('cs0', _('CS0 - Best Effort (0)'));
260 | o.rmempty = false;
261 |
262 | o = s.taboption('general', form.Flag, 'counter', _('Enable counter'));
263 | o.rmempty = false;
264 |
265 | o = s.taboption('general', form.Flag, 'trace', _('Enable trace'));
266 | o.rmempty = false;
267 | o.default = '0'; // Default to disabled
268 | o.description = _('Debug only');
269 |
270 | o = s.taboption('general', form.Flag, 'enabled', _('Enable'));
271 | o.rmempty = false;
272 | o.editable = true;
273 | o.default = '1'; // Set default value to enabled
274 | o.write = function(section_id, formvalue) {
275 | // Always write the value, whether it's '0' or '1'
276 | uci.set('qosmate', section_id, 'enabled', formvalue);
277 | };
278 | o.load = function(section_id) {
279 | var value = uci.get('qosmate', section_id, 'enabled');
280 | // If the value is undefined (not set in config), return '1' (enabled)
281 | return (value === undefined) ? '1' : value;
282 | };
283 |
284 | return m.render();
285 | }
286 | });
287 |
--------------------------------------------------------------------------------
/htdocs/luci-static/resources/view/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | 'require view';
3 | 'require form';
4 | 'require ui';
5 | 'require uci';
6 | 'require rpc';
7 | 'require fs';
8 | 'require poll';
9 | 'require tools.widgets as widgets';
10 |
11 | const UI_VERSION = '1.2.0';
12 | const UI_UPD_CHANNEL = 'release';
13 |
14 | var callInitAction = rpc.declare({
15 | object: 'luci',
16 | method: 'setInitAction',
17 | params: ['name', 'action'],
18 | expect: { result: false }
19 | });
20 |
21 | function createStatusText(status, text) {
22 | var colors = {
23 | 'current': '#4CAF50', // Green
24 | 'update': '#FF5252', // Red
25 | 'error': '#FFC107', // Yellow
26 | 'unknown': '#9E9E9E' // Gray
27 | };
28 |
29 | var icons = {
30 | 'current': '✓ ',
31 | 'update': '↑ ',
32 | 'error': '⚠ ',
33 | 'unknown': '? '
34 | };
35 |
36 | return E('span', {
37 | 'style': 'color: ' + colors[status] + '; font-weight: bold; font-size: 13px;'
38 | }, icons[status] + text);
39 | }
40 |
41 | var healthCheckData = null;
42 | var versionInfo = {
43 | backend: { current: 'Unknown', latest: 'Unknown', channel: 'Unknown' },
44 | frontend: { current: 'Unknown', latest: 'Unknown', channel: 'Unknown' }
45 | };
46 |
47 | function fetchVersionInfo() {
48 | return fs.exec_direct('/etc/init.d/qosmate', ['check_version'])
49 | .then(function(output) {
50 | // Check for API limit error
51 | if (output.includes('HTTP error 403') || output.includes('API rate limit exceeded')) {
52 | console.warn('GitHub API rate limit likely reached.');
53 |
54 | // Try to parse current versions even if latest check failed due to rate limit
55 | const backendCurrentMatch = output.match(/Backend versions:[\s\S]*?Current version: (.+)/);
56 | const backendChannelMatch = output.match(/Backend versions:[\s\S]*?Update channel: (.+)/);
57 | const frontendCurrentMatch = output.match(/Frontend versions:[\s\S]*?Current version: (.+)/);
58 | const frontendChannelMatch = output.match(/Frontend versions:[\s\S]*?Update channel: (.+)/);
59 |
60 | versionInfo = {
61 | backend: {
62 | current: backendCurrentMatch ? backendCurrentMatch[1].trim() : 'Unknown',
63 | latest: 'API limit reached',
64 | channel: backendChannelMatch ? backendChannelMatch[1].trim() : 'Unknown'
65 | },
66 | frontend: {
67 | current: frontendCurrentMatch ? frontendCurrentMatch[1].trim() : 'Unknown',
68 | latest: 'API limit reached',
69 | channel: frontendChannelMatch ? frontendChannelMatch[1].trim() : 'Unknown'
70 | }
71 | };
72 |
73 | } else {
74 | // Normal processing
75 | const backendCurrentMatch = output.match(/Backend versions:[\s\S]*?Current version: (.+)/);
76 | const backendLatestMatch = output.match(/Backend versions:[\s\S]*?Latest version: (.+)/);
77 | const backendChannelMatch = output.match(/Backend versions:[\s\S]*?Update channel: (.+)/);
78 |
79 | const frontendCurrentMatch = output.match(/Frontend versions:[\s\S]*?Current version: (.+)/);
80 | const frontendLatestMatch = output.match(/Frontend versions:[\s\S]*?Latest version: (.+)/);
81 | const frontendChannelMatch = output.match(/Frontend versions:[\s\S]*?Update channel: (.+)/);
82 |
83 | versionInfo = {
84 | backend: {
85 | current: backendCurrentMatch ? backendCurrentMatch[1].trim() : 'Unknown',
86 | latest: backendLatestMatch ? backendLatestMatch[1].trim() : 'Unknown',
87 | channel: backendChannelMatch ? backendChannelMatch[1].trim() : 'Unknown'
88 | },
89 | frontend: {
90 | current: frontendCurrentMatch ? frontendCurrentMatch[1].trim() : 'Unknown',
91 | latest: frontendLatestMatch ? frontendLatestMatch[1].trim() : 'Unknown',
92 | channel: frontendChannelMatch ? frontendChannelMatch[1].trim() : 'Unknown'
93 | }
94 | };
95 | }
96 |
97 | return versionInfo;
98 | })
99 | .catch(function(error) {
100 | console.error('Error fetching version information:', error);
101 | // Keep previous info if available, otherwise set to error state
102 | if (versionInfo.backend.current === 'Unknown' && versionInfo.frontend.current === 'Unknown') {
103 | versionInfo = {
104 | backend: { current: 'Unknown', latest: 'Error', channel: 'Unknown' },
105 | frontend: { current: 'Unknown', latest: 'Error', channel: 'Unknown' }
106 | };
107 | } else {
108 | versionInfo.backend.latest = 'Error';
109 | versionInfo.frontend.latest = 'Error';
110 | }
111 |
112 | return versionInfo;
113 | });
114 | }
115 |
116 | return view.extend({
117 | handleSaveApply: function(ev) {
118 | return this.handleSave(ev)
119 | .then(() => ui.changes.apply())
120 | .then(() => uci.load('qosmate'))
121 | .then(() => uci.get_first('qosmate', 'global', 'enabled'))
122 | .then(enabled => {
123 | if (enabled === '0') {
124 | return fs.exec_direct('/etc/init.d/qosmate', ['stop']);
125 | } else {
126 | return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
127 | }
128 | })
129 | .then(() => {
130 | ui.hideModal();
131 | window.location.reload();
132 | })
133 | .catch(err => {
134 | ui.hideModal();
135 | ui.addNotification(null, E('p', _('Failed to save settings or update QoSmate service: ') + err.message));
136 | });
137 | },
138 |
139 | load: function() {
140 | return Promise.all([
141 | uci.load('qosmate'),
142 | this.fetchHealthCheck(),
143 | fetchVersionInfo()
144 | ]).catch(error => {
145 | console.error('Error in load function:', error);
146 | ui.addNotification(null, E('p', _('Error loading initial data: %s').format(error.message || error)), 'error');
147 | return [null, null, null];
148 | });
149 | },
150 |
151 | fetchHealthCheck: function() {
152 | return fs.exec_direct('/etc/init.d/qosmate', ['health_check'])
153 | .then((res) => {
154 | var output = res.trim();
155 | // Parse the full status string (everything between status= and ;errors=)
156 | var statusMatch = output.match(/status=(.*?);errors=/);
157 | var errorsMatch = output.match(/errors=(\d+)$/);
158 |
159 | var statusString = statusMatch ? statusMatch[1] : 'Unknown';
160 | var errorsCount = errorsMatch ? parseInt(errorsMatch[1]) : 0;
161 |
162 | var statusSegments = statusString.split(';');
163 | var detailsArray = [];
164 | statusSegments.forEach(function(segment) {
165 | if (!segment) return;
166 | detailsArray.push(segment);
167 | });
168 |
169 | healthCheckData = {
170 | details: detailsArray,
171 | errors: errorsCount
172 | };
173 | // console.log("Health check data loaded successfully:", healthCheckData);
174 | })
175 | .catch((err) => {
176 | console.error('Health check failed:', err);
177 | healthCheckData = {
178 | details: ['Health check failed: ' + err],
179 | errors: 1
180 | };
181 | });
182 | },
183 |
184 | render: function() {
185 | var m, s_info, s_status, o;
186 |
187 | m = new form.Map('qosmate', _(''), _(''));
188 |
189 | s_info = m.section(form.NamedSection, 'global', 'global', _('Version & Updates'));
190 | s_info.anonymous = true;
191 |
192 | // Version information
193 | o = s_info.option(form.DummyValue, '_version', _('Version Information'));
194 | o.rawhtml = true;
195 | o.render = function(section_id) {
196 | // Determine if an update is available for backend
197 | var backendUpdateAvailable = versionInfo.backend.current !== versionInfo.backend.latest &&
198 | versionInfo.backend.current !== 'Unknown' &&
199 | versionInfo.backend.latest !== 'Unknown' &&
200 | versionInfo.backend.latest !== 'API limit reached' &&
201 | versionInfo.backend.latest !== 'Error';
202 |
203 | // Determine if an update is available for frontend
204 | var frontendUpdateAvailable = versionInfo.frontend.current !== versionInfo.frontend.latest &&
205 | versionInfo.frontend.current !== 'Unknown' &&
206 | versionInfo.frontend.latest !== 'Unknown' &&
207 | versionInfo.frontend.latest !== 'API limit reached' &&
208 | versionInfo.frontend.latest !== 'Error';
209 |
210 | var container = E('div');
211 |
212 | // Create each component section
213 | function createComponentSection(title, info) {
214 | var section = E('div', { 'style': 'display: flex; align-items: center; margin-bottom: 8px;' });
215 | var titleEl = E('div', {
216 | 'style': 'display: inline-block; min-width: 75px; font-weight: bold; margin-right: 1px;'
217 | }, title);
218 |
219 | section.appendChild(titleEl);
220 |
221 | var channelEl = E('div', {
222 | 'style': 'display: inline-block; min-width: 70px; color: #888; margin-right: 15px;'
223 | }, [
224 | E('span', { 'style': 'font-family: monospace;' }, 'Channel: ' + info.channel)
225 | ]);
226 |
227 | section.appendChild(channelEl);
228 |
229 | var versionInfo = E('div', { 'style': 'display: inline-block; margin-right: 10px;' }, [
230 | info.current,
231 | ' → ',
232 | E('span', {
233 | 'style': (title === 'Backend' && backendUpdateAvailable) ||
234 | (title === 'Frontend' && frontendUpdateAvailable) ?
235 | 'color: #ff7d7d; font-weight: bold;' : ''
236 | }, info.latest)
237 | ]);
238 |
239 | section.appendChild(versionInfo);
240 |
241 | var statusType = '';
242 | var statusText = '';
243 |
244 | if ((title === 'Backend' && backendUpdateAvailable) ||
245 | (title === 'Frontend' && frontendUpdateAvailable)) {
246 | statusType = 'update';
247 | statusText = _('UPDATE AVAILABLE');
248 | } else if (info.latest === 'API limit reached') {
249 | statusType = 'error';
250 | statusText = _('API LIMIT');
251 | } else if (info.latest === 'Error') {
252 | statusType = 'error';
253 | statusText = _('ERROR');
254 | } else if (info.current !== 'Unknown' && info.latest !== 'Unknown') {
255 | statusType = 'current';
256 | statusText = _('CURRENT');
257 | } else {
258 | statusType = 'unknown';
259 | statusText = _('UNKNOWN');
260 | }
261 |
262 | section.appendChild(createStatusText(statusType, statusText));
263 |
264 | return section;
265 | }
266 |
267 | // Create component sections
268 | container.appendChild(createComponentSection('Backend', versionInfo.backend));
269 | container.appendChild(createComponentSection('Frontend', versionInfo.frontend));
270 |
271 | // Add the update button if updates are available
272 | if (backendUpdateAvailable || frontendUpdateAvailable) {
273 | var buttonContainer = E('div', { 'style': 'margin-top: 10px;' });
274 |
275 | var updateButton = E('button', {
276 | 'class': 'cbi-button cbi-button-apply',
277 | 'click': ui.createHandlerFn(this, function() {
278 | // Create update wizard modal
279 | var updateOptions = [];
280 |
281 | if (backendUpdateAvailable) {
282 | updateOptions.push({
283 | name: 'backend',
284 | title: _('Backend'),
285 | current: versionInfo.backend.current,
286 | latest: versionInfo.backend.latest
287 | });
288 | }
289 |
290 | if (frontendUpdateAvailable) {
291 | updateOptions.push({
292 | name: 'frontend',
293 | title: _('Frontend'),
294 | current: versionInfo.frontend.current,
295 | latest: versionInfo.frontend.latest
296 | });
297 | }
298 |
299 | var modalContent = [
300 | E('h4', {}, _('Update QoSmate Components')),
301 | E('p', {}, _('Select components to update:'))
302 | ];
303 |
304 | var checkboxes = {};
305 | updateOptions.forEach(function(option) {
306 | var checkbox = E('input', {
307 | 'type': 'checkbox',
308 | 'id': 'update_' + option.name,
309 | 'name': 'update_' + option.name,
310 | 'checked': 'checked'
311 | });
312 |
313 | checkboxes[option.name] = checkbox;
314 |
315 | modalContent.push(
316 | E('div', { 'class': 'cbi-value' }, [
317 | E('label', { 'class': 'cbi-value-title', 'for': 'update_' + option.name }, option.title),
318 | E('div', { 'class': 'cbi-value-field' }, [
319 | checkbox,
320 | ' ',
321 | option.current,
322 | ' → ',
323 | E('span', { 'style': 'color: #ff7d7d; font-weight: bold;' }, option.latest)
324 | ])
325 | ])
326 | );
327 | });
328 |
329 | // Update channel info
330 | modalContent.push(
331 | E('div', { 'class': 'cbi-value' }, [
332 | E('label', { 'class': 'cbi-value-title' }, _('Update Channel')),
333 | E('div', { 'class': 'cbi-value-field' }, [
334 | E('span', {}, versionInfo.backend.channel === versionInfo.frontend.channel ?
335 | versionInfo.backend.channel :
336 | _('%s (Backend) / %s (Frontend)').format(versionInfo.backend.channel, versionInfo.frontend.channel)),
337 | E('div', { 'style': 'font-size: 90%; color: #888; margin-top: 5px;' },
338 | _('To change the update channel, go to the Advanced tab'))
339 | ])
340 | ])
341 | );
342 |
343 | // Add buttons
344 | modalContent.push(
345 | E('div', { 'class': 'right' }, [
346 | E('button', {
347 | 'class': 'btn',
348 | 'click': ui.hideModal
349 | }, _('Cancel')),
350 | ' ',
351 | E('button', {
352 | 'class': 'cbi-button cbi-button-positive',
353 | 'click': ui.createHandlerFn(this, function() {
354 | var componentsToUpdate = [];
355 |
356 | updateOptions.forEach(function(option) {
357 | if (checkboxes[option.name].checked) {
358 | componentsToUpdate.push(option.name);
359 | }
360 | });
361 |
362 | if (componentsToUpdate.length === 0) {
363 | ui.hideModal();
364 | return;
365 | }
366 |
367 | ui.showModal(_('Updating QoSmate'), [
368 | E('p', { 'class': 'spinning' }, _('Please wait while QoSmate is being updated...'))
369 | ]);
370 |
371 | // Use the current channel from versionInfo
372 | var selectedChannel;
373 | if (versionInfo.backend.channel === versionInfo.frontend.channel) {
374 | selectedChannel = versionInfo.backend.channel;
375 | } else {
376 | // If channels are mixed, show a confirmation dialog
377 | if (!confirm(_('Components are using different update channels (%s/%s). Continue with %s channel?')
378 | .format(versionInfo.backend.channel, versionInfo.frontend.channel, versionInfo.backend.channel))) {
379 | ui.hideModal();
380 | return;
381 | }
382 | selectedChannel = versionInfo.backend.channel;
383 | }
384 |
385 | var updateArgs = ['update'];
386 |
387 | // Add component selection if not updating both components
388 | if (componentsToUpdate.length === 1) {
389 | updateArgs.push('-c', componentsToUpdate[0]);
390 | }
391 |
392 | // Add channel selection
393 | updateArgs.push('-v', selectedChannel);
394 |
395 | console.log('Executing update command:', '/etc/init.d/qosmate', updateArgs);
396 |
397 | return fs.exec_direct('/etc/init.d/qosmate', updateArgs)
398 | .then(function(result) {
399 | console.log('Update command result:', result);
400 | // If result contains error messages, treat it as an error
401 | if (result && (result.includes('error') || result.includes('Error') || result.includes('failed'))) {
402 | ui.hideModal();
403 | ui.addNotification(null, E('p', _('Update process encountered issues: %s').format(result)), 'warning');
404 | return;
405 | }
406 |
407 | // Simulate update completion after 5 seconds
408 | setTimeout(function() {
409 | ui.hideModal();
410 | ui.addNotification(null, E('p', _('QoSmate updated successfully.')), 'success');
411 | window.setTimeout(function() {
412 | location.reload();
413 | }, 1000);
414 | }, 5000);
415 | })
416 | .catch(function(err) {
417 | console.error('Update error:', err);
418 | ui.hideModal();
419 |
420 | var errorMessage = _('Failed to update QoSmate');
421 |
422 | if (err) {
423 | if (typeof err === 'string') {
424 | errorMessage += ': ' + err;
425 | } else if (err.message) {
426 | errorMessage += ': ' + err.message;
427 | } else {
428 | errorMessage += ': ' + JSON.stringify(err);
429 | }
430 | }
431 |
432 | if (selectedChannel === 'snapshot') {
433 | errorMessage += '. ' + _('Note: The "snapshot" channel might not be available in the current repository configuration.');
434 | }
435 |
436 | ui.addNotification(null, E('p', errorMessage), 'error');
437 | });
438 | })
439 | }, _('Update Now'))
440 | ])
441 | );
442 |
443 | ui.showModal(_('QoSmate Update'), modalContent);
444 | })
445 | }, [
446 | E('span', { 'class': 'cbi-button-icon cbi-icon-reload' }),
447 | ' ',
448 | _('Update QoSmate')
449 | ]);
450 |
451 | buttonContainer.appendChild(updateButton);
452 | container.appendChild(buttonContainer);
453 | }
454 |
455 | return E('div', { 'class': 'cbi-value' }, [
456 | E('label', { 'class': 'cbi-value-title' }, _('Version Information')),
457 | E('div', { 'class': 'cbi-value-field' }, [
458 | container
459 | ])
460 | ]);
461 | };
462 |
463 | // Section, also targeting 'global' but with a UI title for grouping
464 | s_status = m.section(form.NamedSection, 'global', 'global', _('Service Status & Control'));
465 |
466 | // Service Status (Health Check)
467 | o = s_status.option(form.DummyValue, '_health_check', _(''));
468 | o.rawhtml = true;
469 | o.render = function(section_id) {
470 | if (!healthCheckData) {
471 | return E('div', { 'class': 'cbi-value' }, [
472 | E('label', { 'class': 'cbi-value-title' }, _('Service Status')),
473 | E('div', { 'class': 'cbi-value-field' }, _('Loading health check status...'))
474 | ]);
475 | }
476 |
477 | var statusHtml = E('div', { 'class': 'health-status', 'style': 'display: flex; gap: 16px; align-items: center;' });
478 |
479 | healthCheckData.details.forEach(function(detail) {
480 | var [type, status] = detail.split(':');
481 | var displayType = type.charAt(0).toUpperCase() + type.slice(1);
482 | var icon, color;
483 | switch(status.toLowerCase()) {
484 | case 'enabled':
485 | case 'started':
486 | icon = '✓';
487 | color = 'green';
488 | break;
489 | case 'disabled':
490 | case 'stopped':
491 | icon = '✕';
492 | color = 'red';
493 | break;
494 | case 'ok':
495 | icon = '✓';
496 | color = 'green';
497 | break;
498 | case 'failed':
499 | icon = '✕';
500 | color = 'red';
501 | break;
502 | case 'missing':
503 | icon = '⚠';
504 | color = 'orange';
505 | break;
506 | default:
507 | icon = '⚠';
508 | color = 'orange';
509 | }
510 |
511 | statusHtml.appendChild(
512 | E('div', { 'style': 'display: flex; align-items: center; gap: 4px;' }, [
513 | E('span', {
514 | 'style': 'color: ' + color + '; font-size: 15px; font-weight: bold; min-width: 20px;'
515 | }, icon),
516 | E('span', {
517 | 'style': 'font-size: 13px; color: #666;'
518 | }, _(displayType)),
519 | ])
520 | );
521 | });
522 |
523 | return E('div', { 'class': 'cbi-value' }, [
524 | E('label', { 'class': 'cbi-value-title' }, _('Service Status')),
525 | E('div', { 'class': 'cbi-value-field' }, statusHtml)
526 | ]);
527 | };
528 |
529 | // Service Control buttons
530 | o = s_status.option(form.DummyValue, '_buttons', _(''));
531 | o.rawhtml = true;
532 | o.render = function(section_id) {
533 | var buttonStyle = 'button cbi-button';
534 | return E('div', { 'class': 'cbi-value' }, [
535 | E('label', { 'class': 'cbi-value-title' }, _('Service Control')),
536 | E('div', { 'class': 'cbi-value-field' }, [
537 | E('button', {
538 | 'class': buttonStyle + ' cbi-button-apply',
539 | 'click': ui.createHandlerFn(this, function() {
540 | return fs.exec_direct('/etc/init.d/qosmate', ['start'])
541 | .then(function() {
542 | ui.addNotification(null, E('p', _('QoSmate started')), 'success');
543 | window.setTimeout(function() { location.reload(); }, 1000);
544 | })
545 | .catch(function(e) { ui.addNotification(null, E('p', _('Failed to start QoSmate: ') + e), 'error'); });
546 | })
547 | }, _('Start')),
548 | ' ',
549 | E('button', {
550 | 'class': buttonStyle + ' cbi-button-neutral',
551 | 'click': ui.createHandlerFn(this, function() {
552 | return fs.exec_direct('/etc/init.d/qosmate', ['restart'])
553 | .then(function() {
554 | ui.addNotification(null, E('p', _('QoSmate restarted')), 'success');
555 | window.setTimeout(function() { location.reload(); }, 1000);
556 | })
557 | .catch(function(e) { ui.addNotification(null, E('p', _('Failed to restart QoSmate: ') + e), 'error'); });
558 | })
559 | }, _('Restart')),
560 | ' ',
561 | E('button', {
562 | 'class': buttonStyle + ' cbi-button-reset',
563 | 'click': ui.createHandlerFn(this, function() {
564 | return fs.exec_direct('/etc/init.d/qosmate', ['stop'])
565 | .then(function() {
566 | ui.addNotification(null, E('p', _('QoSmate stopped')), 'success');
567 | window.setTimeout(function() { location.reload(); }, 1000);
568 | })
569 | .catch(function(e) { ui.addNotification(null, E('p', _('Failed to stop QoSmate: ') + e), 'error'); });
570 | })
571 | }, _('Stop'))
572 | ])
573 | ]);
574 | };
575 |
576 | // Auto Setup Button
577 | o = s_status.option(form.Button, '_auto_setup', _('Auto Setup'));
578 | o.inputstyle = 'apply';
579 | o.inputtitle = _('Start Auto Setup');
580 | o.onclick = ui.createHandlerFn(this, function() {
581 | ui.showModal(_('Auto Setup'), [
582 | E('p', _('This will run a speed test and configure QoSmate automatically.')),
583 | E('div', { 'class': 'cbi-value' }, [
584 | E('label', { 'class': 'cbi-value-title' }, _('Gaming Device IP (optional)')),
585 | E('input', { 'id': 'gaming_ip', 'type': 'text', 'class': 'cbi-input-text' })
586 | ]),
587 | E('div', { 'class': 'right' }, [
588 | E('button', {
589 | 'class': 'btn',
590 | 'click': ui.hideModal
591 | }, _('Cancel')),
592 | ' ',
593 | E('button', {
594 | 'class': 'btn cbi-button-action',
595 | 'click': ui.createHandlerFn(this, function() {
596 | var gamingIp = document.getElementById('gaming_ip').value;
597 | ui.showModal(_('Running Auto Setup'), [
598 | E('p', { 'class': 'spinning' }, _('Please wait while the auto setup is in progress...')),
599 | E('div', { 'style': 'margin-top: 1em; border-top: 1px solid #ccc; padding-top: 1em;' }, [
600 | E('p', { 'style': 'font-weight: bold;' }, _('Note:')),
601 | E('p', _('Router-based speed tests may underestimate actual speeds. These results serve as a starting point and may require manual adjustment for optimal performance.'))
602 | ])
603 | ]);
604 | return fs.exec_direct('/etc/init.d/qosmate', ['auto_setup_noninteractive', gamingIp])
605 | .then(function(res) {
606 | var outputFile = res.trim();
607 | return fs.read(outputFile).then(function(output) {
608 | ui.hideModal();
609 |
610 | var wanInterface = output.match(/Detected WAN interface: (.+)/);
611 | var downloadSpeed = output.match(/Download speed: (.+) Mbit\/s/);
612 | var uploadSpeed = output.match(/Upload speed: (.+) Mbit\/s/);
613 | var downrate = output.match(/DOWNRATE: (.+) kbps/);
614 | var uprate = output.match(/UPRATE: (.+) kbps/);
615 |
616 | if (!downloadSpeed || !uploadSpeed || parseFloat(downloadSpeed[1]) <= 0 || parseFloat(uploadSpeed[1]) <= 0 ||
617 | !downrate || !uprate || parseInt(downrate[1]) <= 0 || parseInt(uprate[1]) <= 0) {
618 | ui.addNotification(null, E('p', _('Invalid speed test results. Please try again or set values manually.')), 'error');
619 | return;
620 | }
621 | var gamingRules = output.match(/Gaming device rules added for IP: (.+)/);
622 |
623 | ui.showModal(_(''), [
624 | E('h2', { 'style': 'text-align:center; margin-bottom: 1em;' }, _('Auto Setup Results')),
625 | E('h3', { 'style': 'margin-bottom: 0.5em;' }, _('Speed Test Results')),
626 | E('p', { 'style': 'color: orange; margin-bottom: 1em;' }, _('Note: Router-based speed tests may underestimate actual speeds. For best results, consider running tests from a LAN device and manually entering the values. These results serve as a starting point.')),
627 | E('div', { 'style': 'display: table; width: 100%;' }, [
628 | E('div', { 'style': 'display: table-row;' }, [
629 | E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('WAN Interface')),
630 | E('div', { 'style': 'display: table-cell; padding: 5px;' }, wanInterface ? wanInterface[1] : _('Not detected'))
631 | ]),
632 | E('div', { 'style': 'display: table-row;' }, [
633 | E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Download Speed')),
634 | E('div', { 'style': 'display: table-cell; padding: 5px;' }, downloadSpeed ? downloadSpeed[1] + ' Mbit/s' : _('Not available'))
635 | ]),
636 | E('div', { 'style': 'display: table-row;' }, [
637 | E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Upload Speed')),
638 | E('div', { 'style': 'display: table-cell; padding: 5px;' }, uploadSpeed ? uploadSpeed[1] + ' Mbit/s' : _('Not available'))
639 | ])
640 | ]),
641 | E('h3', { 'style': 'margin-top: 1em; margin-bottom: 0.5em;' }, _('QoS Configuration')),
642 | E('div', { 'style': 'display: table; width: 100%;' }, [
643 | E('div', { 'style': 'display: table-row;' }, [
644 | E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Download Rate')),
645 | E('div', { 'style': 'display: table-cell; padding: 5px;' }, downrate ? downrate[1] + ' kbps' : _('Not set'))
646 | ]),
647 | E('div', { 'style': 'display: table-row;' }, [
648 | E('div', { 'style': 'display: table-cell; padding: 5px; font-weight: bold;' }, _('Upload Rate')),
649 | E('div', { 'style': 'display: table-cell; padding: 5px;' }, uprate ? uprate[1] + ' kbps' : _('Not set'))
650 | ])
651 | ]),
652 | gamingRules ? E('div', { 'style': 'margin-top: 1em;' }, [
653 | E('div', { 'style': 'font-weight: bold;' }, _('Gaming Rules')),
654 | E('div', {}, _('Added for IP: ') + gamingRules[1])
655 | ]) : '',
656 | E('div', { 'class': 'right', 'style': 'margin-top: 1em;' }, [
657 | E('button', {
658 | 'class': 'btn cbi-button-action',
659 | 'click': ui.createHandlerFn(this, function() {
660 | ui.showModal(_('Applying Changes'), [
661 | E('p', { 'class': 'spinning' }, _('Please wait while the changes are being applied...'))
662 | ]);
663 |
664 | var rootQdisc = uci.get('qosmate', 'settings', 'ROOT_QDISC');
665 | var downrateValue = downrate ? parseInt(downrate[1]) : 0;
666 | var uprateValue = uprate ? parseInt(uprate[1]) : 0;
667 |
668 | if (rootQdisc === 'hfsc' && (downrateValue <= 0 || uprateValue <= 0)) {
669 | ui.hideModal();
670 | ui.addNotification(null, E('p', _('Invalid rates for HFSC. Please set non-zero values manually.')), 'error');
671 | } else {
672 | uci.set('qosmate', 'settings', 'DOWNRATE', downrateValue.toString());
673 | uci.set('qosmate', 'settings', 'UPRATE', uprateValue.toString());
674 |
675 | uci.save()
676 | .then(() => {
677 | return fs.exec_direct('/etc/init.d/qosmate', ['restart']);
678 | })
679 | .then(() => {
680 | ui.hideModal();
681 | ui.addNotification(null, E('p', _('QoSmate settings updated and service restarted.')), 'success');
682 | window.setTimeout(function() {
683 | location.reload();
684 | }, 2000);
685 | })
686 | .catch(function(err) {
687 | ui.hideModal();
688 | ui.addNotification(null, E('p', _('Failed to update settings or restart QoSmate: ') + err), 'error');
689 | });
690 | }
691 | })
692 | }, _('Apply and Reload'))
693 | ])
694 | ]);
695 | });
696 | })
697 | .catch(function(err) {
698 | ui.hideModal();
699 | ui.addNotification(null, E('p', _('Auto setup failed: ') + err), 'error');
700 | });
701 | })
702 | }, _('Start'))
703 | ])
704 | ]);
705 | });
706 |
707 | let s_basic = m.section(form.NamedSection, 'settings', 'settings', _('Basic Settings'));
708 | s_basic.anonymous = true;
709 |
710 | function createOption(name, title, description, placeholder, datatype) {
711 | var opt = s_basic.option(form.Value, name, title, description);
712 | opt.datatype = datatype || 'string';
713 | opt.rmempty = true;
714 | opt.placeholder = placeholder;
715 |
716 | if (datatype === 'uinteger') {
717 | opt.validate = function(section_id, value) {
718 | if (value === '' || value === null) return true;
719 | if (!/^\d+$/.test(value)) return _('Must be a non-negative integer or empty');
720 | var intValue = parseInt(value, 10);
721 | var rootQdisc = this.section.formvalue(section_id, 'ROOT_QDISC');
722 | if (intValue === 0 && rootQdisc === 'hfsc') {
723 | return _('Value must be greater than 0 for HFSC');
724 | }
725 | return true;
726 | };
727 | }
728 | return opt;
729 | }
730 |
731 | var wanInterface = uci.get('qosmate', 'settings', 'WAN') || '';
732 | o = s_basic.option(widgets.DeviceSelect, 'WAN', _('WAN Interface'), _('Select the WAN interface'));
733 | o.rmempty = false;
734 | o.editable = true;
735 | o.default = wanInterface;
736 |
737 | createOption('DOWNRATE', _('Download Rate (kbps)'), _('Set the download rate in kbps'), _('Default: 90000'), 'uinteger');
738 | createOption('UPRATE', _('Upload Rate (kbps)'), _('Set the upload rate in kbps'), _('Default: 45000'), 'uinteger');
739 |
740 | o = s_basic.option(form.ListValue, 'ROOT_QDISC', _('Root Queueing Discipline'), _('Select the root queueing discipline'));
741 | o.value('hfsc', _('HFSC'));
742 | o.value('cake', _('CAKE'));
743 | o.value('hybrid', _('Hybrid'));
744 | o.default = 'hfsc';
745 | o.onchange = function(ev, section_id, value) {
746 | var downrate = this.map.lookupOption('DOWNRATE', section_id)[0];
747 | var uprate = this.map.lookupOption('UPRATE', section_id)[0];
748 | if (downrate && uprate) {
749 | downrate.map.checkDepends();
750 | uprate.map.checkDepends();
751 | }
752 | };
753 |
754 | return m.render();
755 | }
756 | });
757 |
758 | function updateQosmate() {
759 | // Implement the update logic here
760 | ui.showModal(_('Updating QoSmate'), [
761 | E('p', { 'class': 'spinning' }, _('Updating QoSmate. Please wait...'))
762 | ]);
763 |
764 | // Simulating an update process
765 | setTimeout(function() {
766 | ui.hideModal();
767 | window.location.reload();
768 | }, 5000);
769 | }
770 |
--------------------------------------------------------------------------------
/po/README.md:
--------------------------------------------------------------------------------
1 | # QoSmate Translations
2 |
3 | This directory contains translations for the QoSmate LuCI application.
4 |
5 | ## Contributing Translations
6 |
7 | 1. Copy templates/qosmate.pot to xx.po (where xx is your language code)
8 | 2. Translate the strings in your xx.po file
9 | 3. Submit a pull request
10 |
11 | ## Available Languages
12 | - English (default)
13 | - German (de.po) (test)
14 |
15 | ## Creating/Updating Translations
16 | Use these commands to update translations:
17 |
18 | ```bash
19 | # Update .pot template
20 | ./scripts/i18n-scan.pl htdocs > po/templates/qosmate.pot
21 |
22 | # Update existing .po files
23 | ./scripts/i18n-update.pl po/templates/qosmate.pot po/*.po
24 |
--------------------------------------------------------------------------------
/po/de.po:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr ""
3 | "Project-Id-Version: QoSmate\n"
4 | "PO-Revision-Date: 2024-01-09\n"
5 | "Last-Translator: Your Name\n"
6 | "Language: de\n"
7 | "MIME-Version: 1.0\n"
8 | "Content-Type: text/plain; charset=UTF-8\n"
9 | "Content-Transfer-Encoding: 8bit\n"
10 |
11 | #: htdocs/luci-static/resources/view/qosmate/settings.js:25
12 | msgid "QoSmate Settings"
13 | msgstr "QoSmate Einstellungen"
14 |
15 | #: htdocs/luci-static/resources/view/qosmate/settings.js:26
16 | msgid "Configure QoSmate settings."
17 | msgstr "Konfigurieren Sie die QoSmate-Einstellungen."
18 |
19 | #: htdocs/luci-static/resources/view/qosmate/hfsc.js:15
20 | msgid "HFSC Settings"
21 | msgstr "HFSC-Einstellungen"
22 |
--------------------------------------------------------------------------------
/po/templates/qosmate.pot:
--------------------------------------------------------------------------------
1 | msgid ""
2 | msgstr "Content-Type: text/plain; charset=UTF-8"
3 |
4 | #: htdocs/luci-static/resources/view/qosmate/settings.js:25
5 | msgid "QoSmate Settings"
6 | msgstr ""
7 |
8 | #: htdocs/luci-static/resources/view/qosmate/settings.js:26
9 | msgid "Configure QoSmate settings."
10 | msgstr ""
11 |
12 | #: htdocs/luci-static/resources/view/qosmate/hfsc.js:15
13 | msgid "HFSC Settings"
14 | msgstr ""
15 |
--------------------------------------------------------------------------------
/po/zh_Hans/qosmate.po:
--------------------------------------------------------------------------------
1 | "Content-Type: text/plain; charset=UTF-8\n"
2 | "Content-Transfer-Encoding: 8bit\n"
3 | "Language: zh_CN\n"
4 |
5 | msgid "Settings"
6 | msgstr "设置"
7 |
8 | msgid "Basic Settings"
9 | msgstr "基础设置"
10 |
11 | msgid "HFSC"
12 | msgstr "HFSC"
13 |
14 | msgid "CAKE"
15 | msgstr "CAKE"
16 |
17 | msgid "Advanced"
18 | msgstr "高级设置"
19 |
20 | msgid "Rules"
21 | msgstr "规则"
22 |
23 | msgid "Custom Rules"
24 | msgstr "自定义规则"
25 |
26 | msgid "Connections"
27 | msgstr "连接"
28 |
29 | msgid "QoSmate Settings"
30 | msgstr "QoSmate 设置"
31 |
32 | msgid "Configure QoSmate settings."
33 | msgstr "配置 QoSmate 设置。"
34 |
35 | msgid "Service Status"
36 | msgstr "服务状态"
37 |
38 | msgid "Running"
39 | msgstr "运行中"
40 |
41 | msgid "Stopped"
42 | msgstr "已停止"
43 |
44 | msgid "Service Control"
45 | msgstr "服务控制"
46 |
47 | msgid "Start"
48 | msgstr "启动"
49 |
50 | msgid "Restart"
51 | msgstr "重启"
52 |
53 | msgid "Stop"
54 | msgstr "停止"
55 |
56 | msgid "Auto Setup"
57 | msgstr "自动设置"
58 |
59 | msgid "Start Auto Setup"
60 | msgstr "开始自动设置"
61 |
62 | msgid "This will run a speed test and configure QoSmate automatically."
63 | msgstr "这将运行速度测试并自动配置 QoSmate。"
64 |
65 | msgid "Gaming Device IP (optional)"
66 | msgstr "游戏设备 IP(可选)"
67 |
68 | msgid "Invalid speed test results. Please try again or set values manually."
69 | msgstr "无效的速度测试结果。请重试或手动设置值。"
70 |
71 | msgid "QoSmate settings updated and service restarted."
72 | msgstr "QoSmate 设置已更新,服务已重启。"
73 |
74 | msgid "Failed to start QoSmate"
75 | msgstr "启动 QoSmate 失败"
76 |
77 | msgid "Failed to restart QoSmate"
78 | msgstr "重启 QoSmate 失败"
79 |
80 | msgid "QoSmate stopped"
81 | msgstr "QoSmate 已停止"
82 |
83 | msgid "QoSmate started"
84 | msgstr "QoSmate 已启用"
85 |
86 | msgid "Failed to stop QoSmate"
87 | msgstr "停止 QoSmate 失败"
88 |
89 | msgid "Version Information"
90 | msgstr "版本信息"
91 |
92 | msgid "Unable to fetch"
93 | msgstr "获取失败"
94 |
95 | msgid "Current Version"
96 | msgstr "当前版本"
97 |
98 | msgid "Latest Version"
99 | msgstr "最新版本"
100 |
101 | msgid "A new version is available!"
102 | msgstr "有新版本可用!"
103 |
104 | msgid "QoSmate is up to date."
105 | msgstr "QoSmate 已是最新"
106 |
107 | msgid "Unable to check for updates."
108 | msgstr "检查更新失败"
109 |
110 | msgid "Enable"
111 | msgstr "启用"
112 |
113 | msgid "Enable or disable qosmate"
114 | msgstr "启用或禁用 QoSmate"
115 |
116 | msgid "WAN Interface"
117 | msgstr "WAN 接口"
118 |
119 | msgid "Select the WAN interface"
120 | msgstr "选择 WAN 接口"
121 |
122 | msgid "Download Rate (kbps)"
123 | msgstr "下载速度 (kbps)"
124 |
125 | msgid "Set the download rate in kbps"
126 | msgstr "设置下载速率(kbps)"
127 |
128 | msgid "Upload Rate (kbps)"
129 | msgstr "上传速度 (kbps)"
130 |
131 | msgid "Set the upload rate in kbps"
132 | msgstr "设置上传速率(kbps)"
133 |
134 | msgid "Root Queueing Discipline"
135 | msgstr "根队列管理策略"
136 |
137 | msgid "Select the root queueing discipline"
138 | msgstr "选择根队列管理策略"
139 |
140 | msgid "Please set non-zero values manually."
141 | msgstr "请手动设置非零值。"
142 |
143 | msgid "QoSmate HFSC Settings"
144 | msgstr "QoSmate HFSC 设置"
145 |
146 | msgid "Configure HFSC settings for QoSmate."
147 | msgstr "配置 QoSmate 的 HFSC 设置。"
148 |
149 | msgid "HFSC Settings"
150 | msgstr "HFSC 设置"
151 |
152 | msgid "Link Type"
153 | msgstr "链路类型"
154 |
155 | msgid "Select the link type"
156 | msgstr "选择链路类型"
157 |
158 | msgid "Overhead"
159 | msgstr "开销"
160 |
161 | msgid "Set the overhead"
162 | msgstr "设置开销"
163 |
164 | msgid "Default: 44"
165 | msgstr "默认值: 44"
166 |
167 | msgid "Game Queue Discipline"
168 | msgstr "游戏队列机制"
169 |
170 | msgid "Queueing method for traffic classified as realtime"
171 | msgstr "分类为实时流量的队列方法"
172 |
173 | msgid "Game Upload (kbps)"
174 | msgstr "游戏上传速率(kbps)"
175 |
176 | msgid "Bandwidth reserved for realtime upload traffic"
177 | msgstr "为实时上传流量保留的带宽"
178 |
179 | msgid "Default: 15% of UPRATE + 400"
180 | msgstr "默认值: 上传速度的 15% + 400"
181 |
182 | msgid "Default: 15% of DOWNRATE + 400"
183 | msgstr "默认值: 下载速度的 15% + 400"
184 |
185 | msgid "Game Download (kbps)"
186 | msgstr "游戏下载速率(kbps)"
187 |
188 | msgid "Bandwidth reserved for realtime download traffic"
189 | msgstr "为实时下载流量保留的带宽"
190 |
191 | msgid "Non-Game Queue Discipline"
192 | msgstr "非游戏队列机制"
193 |
194 | msgid "Select the queueing discipline for non-realtime traffic"
195 | msgstr "为非实时流量选择队列机制"
196 |
197 | msgid "Non-Game QDisc Options"
198 | msgstr "非游戏队列选项"
199 |
200 | msgid "Cake options for non-realtime queueing discipline"
201 | msgstr "非实时队列机制的 Cake 选项"
202 |
203 | msgid "Default: besteffort ack-filter"
204 | msgstr "默认值: besteffort ack-filter"
205 |
206 | msgid "Max Delay (ms)"
207 | msgstr "最大延迟(毫秒)"
208 |
209 | msgid "Target max delay for realtime packets after burst (pfifo, bfifo, red)"
210 | msgstr "突发后实时数据包的目标最大延迟 (pfifo, bfifo, red)"
211 |
212 | msgid "PFIFO Min"
213 | msgstr "PFIFO 最小值"
214 |
215 | msgid "Minimum packet count for PFIFO queue"
216 | msgstr "PFIFO 队列的最小数据包数"
217 |
218 | msgid "Avg Packet Size (B)"
219 | msgstr "平均数据包大小(字节)"
220 |
221 | msgid "Used with PFIFOMIN to calculate PFIFO limit"
222 | msgstr "与 PFIFOMIN 一起使用以计算 PFIFO 限制"
223 |
224 | msgid "NETEM Delay (ms)"
225 | msgstr "NETEM 延迟(毫秒)"
226 |
227 | msgid "NETEM delay in milliseconds"
228 | msgstr "NETEM 的延迟,以毫秒为单位"
229 |
230 | msgid "NETEM Jitter (ms)"
231 | msgstr "NETEM 抖动(毫秒)"
232 |
233 | msgid "NETEM jitter in milliseconds"
234 | msgstr "NETEM 的抖动,以毫秒为单位"
235 |
236 | msgid "NETEM Distribution"
237 | msgstr "NETEM 分布"
238 |
239 | msgid "NETEM delay distribution"
240 | msgstr "NETEM 延迟分布"
241 |
242 | msgid "Packet Loss Percentage"
243 | msgstr "数据包丢失百分比"
244 |
245 | msgid "Percentage of packet loss"
246 | msgstr "数据包丢失百分比"
247 |
248 | msgid "Failed to save settings or update QoSmate service: "
249 | msgstr "保存设置或更新 QoSmate 服务失败:"
250 |
251 | msgid "QoSmate CAKE Settings"
252 | msgstr "QoSmate CAKE 设置"
253 |
254 | msgid "Configure CAKE settings for QoSmate."
255 | msgstr "配置 QoSmate 的 CAKE 设置。"
256 |
257 | msgid "CAKE Settings"
258 | msgstr "CAKE 设置"
259 |
260 | msgid "Common Link Presets"
261 | msgstr "常见链路预设"
262 |
263 | msgid "Select common link presets"
264 | msgstr "选择常见链路预设"
265 |
266 | msgid "Raw (No overhead compensation)"
267 | msgstr "原始(无开销补偿)"
268 |
269 | msgid "Conservative (48 bytes overhead + ATM)"
270 | msgstr "保守(48 字节开销 + ATM)"
271 |
272 | msgid "Ethernet"
273 | msgstr "以太网"
274 |
275 | msgid "DOCSIS cable"
276 | msgstr "DOCSIS 电缆"
277 |
278 | msgid "PPPoA VC-Mux"
279 | msgstr "PPPoA VC-Mux"
280 |
281 | msgid "PPPoA LLC"
282 | msgstr "PPPoA LLC"
283 |
284 | msgid "PPPoE VC-Mux"
285 | msgstr "PPPoE VC-Mux"
286 |
287 | msgid "PPPoE LLC-SNAP"
288 | msgstr "PPPoE LLC-SNAP"
289 |
290 | msgid "Bridged VC-Mux"
291 | msgstr "桥接 VC-Mux"
292 |
293 | msgid "Bridged LLC-SNAP"
294 | msgstr "桥接 LLC-SNAP"
295 |
296 | msgid "IPoA VC-Mux"
297 | msgstr "IPoA VC-Mux"
298 |
299 | msgid "IPoA LLC-SNAP"
300 | msgstr "IPoA LLC-SNAP"
301 |
302 | msgid "PPPoE PTM"
303 | msgstr "PPPoE PTM"
304 |
305 | msgid "Bridged PTM"
306 | msgstr "桥接 PTM"
307 |
308 | msgid "Set the overhead"
309 | msgstr "设置开销"
310 |
311 | msgid "Default: based on preset"
312 | msgstr "默认值: 基于预设"
313 |
314 | msgid "MPU"
315 | msgstr "MPU"
316 |
317 | msgid "Minimum packet size CAKE will account for"
318 | msgstr "CAKE 将考虑的最小数据包大小"
319 |
320 | msgid "Link Compensation"
321 | msgstr "链路补偿"
322 |
323 | msgid "Set the link compensation"
324 | msgstr "设置链路补偿"
325 |
326 | msgid "Ether VLAN Keyword"
327 | msgstr "Ether VLAN 关键字"
328 |
329 | msgid "Set the Ether VLAN keyword"
330 | msgstr "设置 Ether VLAN 关键字"
331 |
332 | msgid "Priority Queue (Ingress)"
333 | msgstr "优先级队列(入口)"
334 |
335 | msgid "Sets CAKE's diffserv mode for incoming traffic"
336 | msgstr "为入站流量设置 CAKE 的 diffserv 模式"
337 |
338 | msgid "Diffserv 3-tier priority"
339 | msgstr "Diffserv 3 级优先级"
340 |
341 | msgid "Diffserv 4-tier priority"
342 | msgstr "Diffserv 4 级优先级"
343 |
344 | msgid "Diffserv 8-tier priority"
345 | msgstr "Diffserv 8 级优先级"
346 |
347 | msgid "Priority Queue (Egress)"
348 | msgstr "优先级队列(出口)"
349 |
350 | msgid "Sets CAKE's diffserv mode for outgoing traffic"
351 | msgstr "为出站流量设置 CAKE 的 diffserv 模式"
352 |
353 | msgid "Host Isolation"
354 | msgstr "主机隔离"
355 |
356 | msgid "Applies fairness first by host, then by flow(dual-srchost/dual-dsthost)"
357 | msgstr "首先按主机,然后按流量应用公平性 (dual-srchost/dual-dsthost)"
358 |
359 | msgid "NAT (Ingress)"
360 | msgstr "NAT(入口)"
361 |
362 | msgid "Enable NAT lookup for ingress"
363 | msgstr "为入站启用 NAT 查找"
364 |
365 | msgid "NAT (Egress)"
366 | msgstr "NAT(出口)"
367 |
368 | msgid "Enable NAT lookup for egress"
369 | msgstr "为出站启用 NAT 查找"
370 |
371 | msgid "ACK Filter (Egress)"
372 | msgstr "ACK 过滤器(出口)"
373 |
374 | msgid "Set ACK filter for egress. Auto enables filtering if download/upload ratio ≥ 15."
375 | msgstr "为出口设置 ACK 过滤器。如果下载/上传比率≥15,将自动启用过滤。"
376 |
377 | msgid "Auto"
378 | msgstr "自动"
379 |
380 | msgid "Enable"
381 | msgstr "启用"
382 |
383 | msgid "Disable"
384 | msgstr "禁用"
385 |
386 | msgid "RTT"
387 | msgstr "RTT"
388 |
389 | msgid "Default: auto"
390 | msgstr "默认值:自动"
391 |
392 | msgid "Set the Round Trip Time"
393 | msgstr "设置往返时间"
394 |
395 | msgid "Autorate (Ingress)"
396 | msgstr "自动速率(入口)"
397 |
398 | msgid "Enable autorate for ingress"
399 | msgstr "为入站启用自动速率"
400 |
401 | msgid "Extra Parameters (Ingress)"
402 | msgstr "额外参数(入口)"
403 |
404 | msgid "Set extra parameters for ingress"
405 | msgstr "设置入站的额外参数"
406 |
407 | msgid "Extra Parameters (Egress)"
408 | msgstr "额外参数(出口)"
409 |
410 | msgid "Set extra parameters for egress"
411 | msgstr "设置出站的额外参数"
412 |
413 | msgid "Failed to save settings or update QoSmate service: "
414 | msgstr "保存设置或更新 QoSmate 服务失败:"
415 |
416 | msgid "QoSmate Advanced Settings"
417 | msgstr "QoSmate 高级设置"
418 |
419 | msgid "Configure advanced settings for QoSmate."
420 | msgstr "配置 QoSmate 的高级设置。"
421 |
422 | msgid "Advanced Settings"
423 | msgstr "高级设置"
424 |
425 | msgid "Preserve Config Files"
426 | msgstr "保留配置文件"
427 |
428 | msgid "Preserve configuration files during system upgrade"
429 | msgstr "系统升级时保留配置文件"
430 |
431 | msgid "Wash DSCP Egress"
432 | msgstr "清除 DSCP(出口)"
433 |
434 | msgid "Sets DSCP to CS0 for outgoing packets after classification"
435 | msgstr "分类后将出口数据包的 DSCP 设置为 CS0"
436 |
437 | msgid "Wash DSCP Ingress"
438 | msgstr "清除 DSCP(入口)"
439 |
440 | msgid "Sets DSCP to CS0 for incoming packets before classification"
441 | msgstr "分类前将入口数据包的 DSCP 设置为 CS0"
442 |
443 | msgid "Bandwidth Max Ratio"
444 | msgstr "带宽最大比例"
445 |
446 | msgid "Max download/upload ratio to prevent upstream congestion"
447 | msgstr "最大下载/上传比例,以防止上游拥塞"
448 |
449 | msgid "Default: 20"
450 | msgstr "默认值:20"
451 |
452 | msgid "ACK Rate"
453 | msgstr "ACK 速率"
454 |
455 | msgid "Sets rate limit for TCP ACKs, helps prevent ACK flooding / set to 0 to disable ACK rate limit"
456 | msgstr "设置 TCP ACK 的速率限制,帮助防止 ACK 泛洪 / 设为 0 以禁用 ACK 速率限制"
457 |
458 | msgid "Default: 5% of UPRATE"
459 | msgstr "默认值:上传速率的 5%"
460 |
461 | msgid "Enable UDP Rate Limit"
462 | msgstr "启用 UDP 速率限制"
463 |
464 | msgid "Downgrades UDP traffic exceeding 450 pps to lower priority"
465 | msgstr "将超过 450 pps 的 UDP 流量降级为低优先级"
466 |
467 | msgid "Boost Low-Volume TCP Traffic"
468 | msgstr "提升小流量 TCP 流量"
469 |
470 | msgid "Upgrade DSCP to AF42 for TCP connections with less than 150 packets per second. This can improve responsiveness for interactive TCP services like SSH, web browsing, and instant messaging."
471 | msgstr "将每秒小于 150 个数据包的 TCP 连接的 DSCP 升级到 AF42。这样可以提高 SSH、网页浏览和即时消息等交互式 TCP 服务的响应速度。"
472 |
473 | msgid "UDP Bulk Ports"
474 | msgstr "UDP 批量端口"
475 |
476 | msgid "Specify UDP ports for bulk traffic"
477 | msgstr "指定用于批量流量的 UDP 端口"
478 |
479 | msgid "Default: none"
480 | msgstr "默认值:无"
481 |
482 | msgid "TCP Bulk Ports"
483 | msgstr "TCP 批量端口"
484 |
485 | msgid "Specify TCP ports for bulk traffic"
486 | msgstr "指定用于批量流量的 TCP 端口"
487 |
488 | msgid "TCP MSS"
489 | msgstr "TCP MSS"
490 |
491 | msgid "Nftables Hook"
492 | msgstr "Nftables 钩子"
493 |
494 | msgid "Select the nftables hook point for the dscptag chain"
495 | msgstr "为 dscptag 链选择 nftables 钩子点"
496 |
497 | msgid "Nftables Priority"
498 | msgstr "Nftables 优先级"
499 |
500 | msgid "Set the priority for the nftables chain. Lower values are processed earlier. Default is 0 | mangle is -150."
501 | msgstr "为 nftables 链选择优先级。数值越低,处理优先级越高。默认为0,mangle表为 -150。"
502 |
503 | msgid "Maximum Segment Size for TCP connections. This setting is only active when the upload or download bandwidth is less than 3000 kbit/s. Leave empty to use the default value."
504 | msgstr "TCP 连接的最大段大小。此设置仅在上传或下载带宽小于 3000 kbit/s 时生效。留空将使用默认值。"
505 |
506 | msgid "Default: 536"
507 | msgstr "默认值:536"
508 |
509 | msgid "Failed to save settings or update QoSmate service: "
510 | msgstr "保存设置或更新 QoSmate 服务失败:"
511 |
512 | msgid "QoSmate Rules"
513 | msgstr "QoSmate 规则"
514 |
515 | msgid "Configure QoS rules for marking packets with DSCP values."
516 | msgstr "配置 QoS 规则以标记具有 DSCP 值的数据包。"
517 |
518 | msgid "Rules"
519 | msgstr "规则"
520 |
521 | msgid "General Settings"
522 | msgstr "常规设置"
523 |
524 | msgid "DSCP Mapping"
525 | msgstr "DSCP 映射"
526 |
527 | msgid "HFSC Mapping:"
528 | msgstr "HFSC 映射:"
529 |
530 | msgid "High Priority [Realtime] (1:11)"
531 | msgstr "高优先级 [实时] (1:11)"
532 |
533 | msgid "EF, CS5, CS6, CS7"
534 | msgstr "EF, CS5, CS6, CS7"
535 |
536 | msgid "Fast Non-Realtime (1:12)"
537 | msgstr "快速非实时 (1:12)"
538 |
539 | msgid "CS4, AF41, AF42"
540 | msgstr "CS4, AF41, AF42"
541 |
542 | msgid "Normal (1:13)"
543 | msgstr "普通 (1:13)"
544 |
545 | msgid "CS0"
546 | msgstr "CS0"
547 |
548 | msgid "Low Priority (1:14)"
549 | msgstr "低优先级 (1:14)"
550 |
551 | msgid "CS2"
552 | msgstr "CS2"
553 |
554 | msgid "Bulk (1:15)"
555 | msgstr "批量 (1:15)"
556 |
557 | msgid "CS1"
558 | msgstr "CS1"
559 |
560 | msgid "CAKE Mapping (diffserv4):"
561 | msgstr "CAKE 映射 (diffserv4):"
562 |
563 | msgid "Voice (Highest Priority)"
564 | msgstr "语音 (最高优先级)"
565 |
566 | msgid "CS7, CS6, EF, VA, CS5, CS4"
567 | msgstr "CS7, CS6, EF, VA, CS5, CS4"
568 |
569 | msgid "Video"
570 | msgstr "视频"
571 |
572 | msgid "CS3, AF4x, AF3x, CS2, TOS1"
573 | msgstr "CS3, AF4x, AF3x, CS2, TOS1"
574 |
575 | msgid "Best Effort"
576 | msgstr "最佳努力"
577 |
578 | msgid "CS0, AF1x, AF2x, TOS0"
579 | msgstr "CS0, AF1x, AF2x, TOS0"
580 |
581 | msgid "Bulk (Lowest Priority)"
582 | msgstr "批量 (最低优先级)"
583 |
584 | msgid "CS1, LE"
585 | msgstr "CS1, LE"
586 |
587 | msgid "Name"
588 | msgstr "名称"
589 |
590 | msgid "Protocol"
591 | msgstr "协议"
592 |
593 | msgid "TCP"
594 | msgstr "TCP"
595 |
596 | msgid "UDP"
597 | msgstr "UDP"
598 |
599 | msgid "ICMP"
600 | msgstr "ICMP"
601 |
602 | msgid "Any"
603 | msgstr "任意"
604 |
605 | msgid "Source IP"
606 | msgstr "源 IP"
607 |
608 | msgid "any"
609 | msgstr "任意"
610 |
611 | msgid "Invalid IP address or hostname"
612 | msgstr "无效的 IP 地址或主机名"
613 |
614 | msgid "Source port"
615 | msgstr "源端口"
616 |
617 | msgid "Invalid port or port range"
618 | msgstr "无效的端口或端口范围"
619 |
620 | msgid "Destination IP"
621 | msgstr "目的 IP"
622 |
623 | msgid "Destination port"
624 | msgstr "目的端口"
625 |
626 | msgid "DSCP Class"
627 | msgstr "DSCP 类别"
628 |
629 | msgid "EF - Expedited Forwarding (46)"
630 | msgstr "EF - 加速转发 (46)"
631 |
632 | msgid "CS5 (40)"
633 | msgstr "CS5 (40)"
634 |
635 | msgid "CS6 (48)"
636 | msgstr "CS6 (48)"
637 |
638 | msgid "CS7 (56)"
639 | msgstr "CS7 (56)"
640 |
641 | msgid "CS4 (32)"
642 | msgstr "CS4 (32)"
643 |
644 | msgid "AF41 (34)"
645 | msgstr "AF41 (34)"
646 |
647 | msgid "AF42 (36)"
648 | msgstr "AF42 (36)"
649 |
650 | msgid "CS2 (16)"
651 | msgstr "CS2 (16)"
652 |
653 | msgid "CS1 (8)"
654 | msgstr "CS1 (8)"
655 |
656 | msgid "CS0 - Best Effort (0)"
657 | msgstr "CS0 - 最佳努力 (0)"
658 |
659 | msgid "Enable counter"
660 | msgstr "启用计数器"
661 |
662 | msgid "Enable"
663 | msgstr "启用"
664 |
665 | msgid "Failed to save settings or update QoSmate service: "
666 | msgstr "保存设置或更新 QoSmate 服务失败:"
667 |
668 | msgid "Custom rules have been saved and applied."
669 | msgstr "自定义规则已保存并应用。"
670 |
671 | msgid "Failed to save settings or restart QoSmate:"
672 | msgstr "无法保存设置或重启 QoSmate:"
673 |
674 | msgid "QoSmate Custom Rules"
675 | msgstr "QoSmate 自定义规则"
676 |
677 | msgid "Define custom nftables rules for advanced traffic control."
678 | msgstr "定义用于高级流量控制的自定义 nftables 规则。"
679 |
680 | msgid "Erase Rules"
681 | msgstr "清除规则"
682 |
683 | msgid "Erase Custom Rules"
684 | msgstr "清除自定义规则"
685 |
686 | msgid "Are you sure you want to erase all custom rules? This action cannot be undone."
687 | msgstr "确定要清除所有自定义规则吗?此操作无法撤销。"
688 |
689 | msgid "Custom rules have been erased and changes applied."
690 | msgstr "自定义规则已清除并应用更改。"
691 |
692 | msgid "Failed to erase custom rules or apply changes:"
693 | msgstr "无法清除自定义规则或应用更改:"
694 |
695 | msgid "Custom nftables Rules"
696 | msgstr "自定义 nftables 规则"
697 |
698 | msgid "Enter your custom nftables rules here. The \"table inet qosmate_custom { ... }\" wrapper will be added automatically."
699 | msgstr "在此处输入您的自定义 nftables 规则。\"table inet qosmate_custom { ... }\" 包装器将被自动添加。"
700 |
701 | msgid "Custom rules validation successful."
702 | msgstr "自定义规则验证成功。"
703 |
704 | msgid "Custom rules validation failed. Please check the validation result below."
705 | msgstr "自定义规则验证失败。请检查下方的验证结果。"
706 |
707 | msgid "Validation Result"
708 | msgstr "验证结果"
709 |
710 | msgid "No validation performed yet"
711 | msgstr "还未进行任何验证"
712 |
713 | msgid "Validate Rules"
714 | msgstr "验证规则"
715 |
716 | msgid "Validate"
717 | msgstr "验证"
718 |
719 | msgid "Rules Information"
720 | msgstr "规则信息"
721 |
722 | msgid "Error: Could not find custom rules textarea"
723 | msgstr "错误:找不到自定义规则文本区域"
724 |
725 | msgid "Validating Rules"
726 | msgstr "正在验证规则"
727 |
728 | msgid "Please wait while the rules are being validated..."
729 | msgstr "请等待规则验证... "
730 |
731 | msgid "Finalizing validation results, please wait..."
732 | msgstr "正在完成验证结果,请稍候..."
733 |
734 | msgid "Error during validation:"
735 | msgstr "验证过程中出错:"
736 |
737 | msgid "Example rules:"
738 | msgstr "示例规则:"
739 |
740 | msgid "Warning:"
741 | msgstr "警告: "
742 |
743 | msgid "Incorrect rules can disrupt network functionality. Use with caution."
744 | msgstr "不正确的规则可能会破坏网络功能。请谨慎使用。"
745 |
746 | msgid "QoSmate Connections"
747 | msgstr "QoSmate 连接"
748 |
749 | msgid "Filter by IP, IP:Port, Port, Protocol or DSCP"
750 | msgstr "通过 IP、IP:端口、端口、协议或 DSCP 过滤"
751 |
752 | msgid "Zoom:"
753 | msgstr "缩放:"
754 |
755 | msgid "In: "
756 | msgstr "入站:"
757 |
758 | msgid "Out: "
759 | msgstr "出站:"
760 |
761 | msgid "Packets"
762 | msgstr "数据包"
763 |
764 | msgid "Avg PPS"
765 | msgstr "平均 PPS"
766 |
767 | msgid "Max PPS"
768 | msgstr "最大 PPS"
769 |
770 | msgid "Avg BPS"
771 | msgstr "平均 BPS"
772 |
773 | msgid "Protocol"
774 | msgstr "协议"
775 |
776 | msgid "Source"
777 | msgstr "来源"
778 |
779 | msgid "Destination"
780 | msgstr "目的地"
781 |
782 | msgid "DSCP"
783 | msgstr "DSCP"
784 |
785 | msgid "Bytes"
786 | msgstr "字节"
787 |
--------------------------------------------------------------------------------
/root/usr/libexec/rpcd/luci.qosmate:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lua
2 |
3 | local jsonc = require "luci.jsonc"
4 |
5 | -- Check if an IP address is private - not needed at the moment
6 | local function is_private_ip(ip)
7 | if not ip or ip == "" then
8 | return false
9 | end
10 |
11 | if ip:match(":") then -- IPv6
12 | -- Check for ULA (fc00::/7), link-local (fe80::/10), or loopback (::1)
13 | return ip:match("^f[cd]") or ip:match("^fe80:") or ip == "::1"
14 | else -- IPv4
15 | local octets = {ip:match("(%d+)%.(%d+)%.(%d+)%.(%d+)")}
16 | if not octets[1] then return false end
17 | local first, second = tonumber(octets[1]), tonumber(octets[2])
18 | -- Check for private IPv4 ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
19 | return (first == 10) or
20 | (first == 172 and second >= 16 and second <= 31) or
21 | (first == 192 and second == 168)
22 | end
23 | end
24 |
25 | -- Parse a single connection line from nf_conntrack
26 | local function parse_connection(line)
27 | local conn = {}
28 | conn.layer3, conn.layer4, conn.protocol, conn.timeout = line:match("(%w+)%s+(%d+)%s+(%w+)%s+(%d+)")
29 | if not (conn.layer3 and conn.layer4 and conn.protocol and conn.timeout) then
30 | return nil
31 | end
32 |
33 | if conn.protocol == "icmp" or conn.protocol == "icmpv6" then
34 | -- Handle ICMP and ICMPv6 protocols
35 | conn.src, conn.dst, conn.type, conn.code, conn.id =
36 | line:match("src=(%S+)%s+dst=(%S+)%s+type=(%d+)%s+code=(%d+)%s+id=(%d+)")
37 | conn.in_packets, conn.in_bytes = line:match("packets=(%d+)%s+bytes=(%d+)")
38 | conn.out_packets, conn.out_bytes = conn.in_packets, conn.in_bytes
39 | conn.sport, conn.dport = "-", "-" -- ICMP doesn't use ports
40 | else
41 | -- Handle other protocols (e.g., TCP, UDP)
42 | local src1, dst1, sport1, dport1, packets1, bytes1,
43 | src2, dst2, sport2, dport2, packets2, bytes2 =
44 | line:match("src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+).*src=(%S+)%s+dst=(%S+)%s+sport=(%d+)%s+dport=(%d+)%s+packets=(%d+)%s+bytes=(%d+)")
45 |
46 | -- Always use the first direction as the original
47 | conn.src, conn.dst, conn.sport, conn.dport = src1, dst1, sport1, dport1
48 | conn.out_packets, conn.in_packets = tonumber(packets1), tonumber(packets2)
49 | conn.out_bytes, conn.in_bytes = tonumber(bytes1), tonumber(bytes2)
50 | end
51 |
52 | if not (conn.src and conn.dst) then
53 | return nil
54 | end
55 |
56 | -- Extract additional connection information
57 | conn.dscp = math.floor(tonumber(line:match("mark=(%d+)") or "0"))
58 | conn.use = tonumber(line:match("use=(%d+)") or "0")
59 | conn.timeout = tonumber(conn.timeout) or 0
60 |
61 | -- Determine connection state
62 | if line:match("%[ASSURED%]") then
63 | conn.state = "ASSURED"
64 | elseif line:match("%[UNREPLIED%]") then
65 | conn.state = "UNREPLIED"
66 | else
67 | conn.state = "UNKNOWN"
68 | end
69 |
70 | return conn
71 | end
72 |
73 | -- Retrieve all current connections from nf_conntrack
74 | local function get_connections()
75 | local conn_list = {}
76 | local f = io.open("/proc/net/nf_conntrack", "r")
77 | if not f then
78 | return {}
79 | end
80 |
81 | -- Read all connections from nf_conntrack
82 | for line in f:lines() do
83 | local conn = parse_connection(line)
84 | if conn then
85 | conn.total_bytes = (conn.in_bytes or 0) + (conn.out_bytes or 0)
86 | table.insert(conn_list, conn)
87 | end
88 | end
89 | f:close()
90 |
91 | -- Sort the entire list in descending order by total_bytes
92 | table.sort(conn_list, function(a, b)
93 | return a.total_bytes > b.total_bytes
94 | end)
95 |
96 | -- Determine max_entries based on system load
97 | local load_avg = io.open("/proc/loadavg"):read("*a")
98 | local load1 = tonumber(load_avg:match("^(%d+%.%d+)")) or 0
99 |
100 | -- Default: no limit
101 | local max_entries = math.huge
102 |
103 | -- Simple thresholds based on absolute load
104 | if load1 > 2.0 then
105 | max_entries = 100
106 | -- io.popen("logger -t qosmate 'Critical load! Limiting to 100 connections'")
107 | elseif load1 > 1.5 then
108 | max_entries = 500
109 | -- io.popen("logger -t qosmate 'High load! Limiting to 500 connections'")
110 | elseif load1 > 0.8 then
111 | max_entries = 1000
112 | -- io.popen("logger -t qosmate 'Moderate load! Limiting to 1000 connections'")
113 | end
114 |
115 | -- Final connections table
116 | local connections = {}
117 | for i = 1, math.min(max_entries, #conn_list) do
118 | local conn = conn_list[i]
119 | local key = string.format("%s:%s:%s:%s:%s:%s",
120 | conn.layer3, conn.protocol, conn.src, conn.sport, conn.dst, conn.dport)
121 | connections[key] = conn
122 | end
123 |
124 | return connections
125 | end
126 |
127 | -- Define RPC methods
128 | local methods = {
129 | getConntrackDSCP = {
130 | call = function()
131 | local connections = get_connections()
132 | return {result = jsonc.stringify({connections = connections})}
133 | end
134 | }
135 | }
136 |
137 | -- Handle RPC calls
138 | if arg[1] == "list" then
139 | local rv = {}
140 | for _, v in pairs(methods) do
141 | rv[_] = v.args or {}
142 | end
143 | print((jsonc.stringify(rv):gsub(":%[%]", ":{}")))
144 | elseif arg[1] == "call" then
145 | local args = jsonc.parse(io.stdin:read("*a"))
146 | local method = methods[arg[2]]
147 | if method then
148 | local result = method.call(args)
149 | print(result.result)
150 | os.exit(result.code or 0)
151 | else
152 | print(jsonc.stringify({error = "Method not found"}))
153 | os.exit(1)
154 | end
155 | end
156 |
--------------------------------------------------------------------------------
/root/usr/libexec/rpcd/luci.qosmate_stats:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # QoSmate Statistics Backend for LuCI
4 | # shellcheck disable=SC2039,SC3043,SC2155,SC3057,SC1091
5 |
6 | . /lib/functions.sh
7 |
8 | # Set to 1 to enable debug logging, 0 to disable
9 | DEBUG=0
10 |
11 | # Debug logging function
12 | debug_log() {
13 | if [ "$DEBUG" -eq 1 ]; then
14 | echo "$1" >> /tmp/qosmate_debug.log
15 | fi
16 | }
17 |
18 | # Function to safely get JSON from tc command
19 | get_tc_json() {
20 | local output
21 | # Fix HFSC options format by completely replacing the options object
22 | output=$("$@" 2>/dev/null) || echo "[]"
23 |
24 | # Check if output is empty
25 | if [ -z "$output" ]; then
26 | echo "[]"
27 | return
28 | fi
29 |
30 | # Fix the HFSC options format by replacing the entire options object with a simple default value
31 | echo "$output" | sed 's/\("kind":"hfsc",[^{]*"options":\){[^}]*}/\1{"default":13}/g'
32 | }
33 |
34 | # Function to filter statistics to only include QoSmate relevant qdiscs
35 | filter_qosmate_qdiscs() {
36 | local stats="$1"
37 | local wan_interface="$2"
38 | local ifb_interface="$3"
39 |
40 | # Filter to only include qdiscs related to QoSmate (on wan_interface and ifb_interface)
41 | echo "$stats" | jq -c "[.[] | select(.dev == \"$wan_interface\" or .dev == \"$ifb_interface\")]" 2>/dev/null || echo "[]"
42 | }
43 |
44 | # Function to get the statistics based on root qdisc
45 | get_stats() {
46 | # Get basic statistics for all qdiscs
47 | local basic_stats
48 | basic_stats=$(get_tc_json tc -s -j qdisc show)
49 |
50 | debug_log "Basic stats: $basic_stats"
51 |
52 | # Get root qdisc type
53 | local root_qdisc
54 | root_qdisc=$(uci -q get qosmate.settings.ROOT_QDISC || echo "hfsc")
55 |
56 | # Get interface names
57 | local wan_interface
58 | wan_interface=$(uci -q get qosmate.settings.WAN || echo "eth1")
59 |
60 | local ifb_interface
61 | ifb_interface="ifb-$wan_interface"
62 |
63 | # Filter basic stats to only include QoSmate relevant qdiscs
64 | local filtered_stats
65 | filtered_stats=$(filter_qosmate_qdiscs "$basic_stats" "$wan_interface" "$ifb_interface")
66 | debug_log "Filtered stats: $filtered_stats"
67 |
68 | # Process statistics based on qdisc type
69 | local result
70 | if [ "$root_qdisc" = "cake" ]; then
71 | # Extract CAKE statistics for both egress and ingress
72 | local cake_egress_stats
73 | cake_egress_stats=$(echo "$filtered_stats" | jq -c "[.[] | select(.kind == \"cake\" and .dev == \"$wan_interface\")]" 2>/dev/null || echo "[]")
74 | debug_log "Cake egress stats: $cake_egress_stats"
75 |
76 | local cake_ingress_stats
77 | cake_ingress_stats=$(echo "$filtered_stats" | jq -c "[.[] | select(.kind == \"cake\" and .dev == \"$ifb_interface\")]" 2>/dev/null || echo "[]")
78 | debug_log "Cake ingress stats: $cake_ingress_stats"
79 |
80 | # Get the priority queue type (diffserv3/4/8)
81 | local priority_queue_ingress
82 | priority_queue_ingress=$(uci -q get qosmate.cake.PRIORITY_QUEUE_INGRESS || echo "diffserv4")
83 | local priority_queue_egress
84 | priority_queue_egress=$(uci -q get qosmate.cake.PRIORITY_QUEUE_EGRESS || echo "diffserv4")
85 |
86 | # Extract the actual cake qdisc stats for UI compatibility
87 | local cake_egress=$(echo "$cake_egress_stats" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
88 | local cake_ingress=$(echo "$cake_ingress_stats" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
89 |
90 | # Create final JSON output using jq to ensure proper escaping
91 | result=$(jq -n \
92 | --arg rq "$root_qdisc" \
93 | --arg wi "$wan_interface" \
94 | --arg ii "$ifb_interface" \
95 | --arg pqi "$priority_queue_ingress" \
96 | --arg pqe "$priority_queue_egress" \
97 | --argjson fs "$filtered_stats" \
98 | --argjson ce "$cake_egress" \
99 | --argjson ci "$cake_ingress" \
100 | '{
101 | root_qdisc: $rq,
102 | wan_interface: $wi,
103 | ifb_interface: $ii,
104 | priority_queue_ingress: $pqi,
105 | priority_queue_egress: $pqe,
106 | qdisc_stats: $fs,
107 | cake_egress: $ce,
108 | cake_ingress: $ci
109 | }' 2>/dev/null)
110 | elif [ "$root_qdisc" = "hfsc" ]; then
111 | # Extract HFSC root qdiscs and classes
112 | local hfsc_egress_qdisc
113 | hfsc_egress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$wan_interface" | jq -c "[.[] | select(.kind == \"hfsc\")]" 2>/dev/null || echo "[]")
114 | debug_log "HFSC egress qdisc: $hfsc_egress_qdisc"
115 |
116 | local hfsc_egress_classes
117 | hfsc_egress_classes=$(get_tc_json tc -s -j class show dev "$wan_interface")
118 | debug_log "HFSC egress classes: $hfsc_egress_classes"
119 |
120 | # Process the output to ensure it's valid JSON
121 | hfsc_egress_classes=$(echo "$hfsc_egress_classes" | jq -c '.' 2>/dev/null || echo "[]")
122 |
123 | local hfsc_ingress_qdisc
124 | hfsc_ingress_qdisc=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface" | jq -c "[.[] | select(.kind == \"hfsc\")]" 2>/dev/null || echo "[]")
125 | debug_log "HFSC ingress qdisc: $hfsc_ingress_qdisc"
126 |
127 | local hfsc_ingress_classes
128 | hfsc_ingress_classes=$(get_tc_json tc -s -j class show dev "$ifb_interface")
129 | debug_log "HFSC ingress classes: $hfsc_ingress_classes"
130 |
131 | # Process the output to ensure it's valid JSON
132 | hfsc_ingress_classes=$(echo "$hfsc_ingress_classes" | jq -c '.' 2>/dev/null || echo "[]")
133 |
134 | # Get leaf qdisc statistics
135 | local egress_leaf_qdiscs
136 | egress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$wan_interface")
137 | debug_log "Egress leaf qdiscs: $egress_leaf_qdiscs"
138 |
139 | # Process the output to ensure it's valid JSON and filter non-HFSC qdiscs
140 | egress_leaf_qdiscs=$(echo "$egress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "hfsc" and .parent != null)]' 2>/dev/null || echo "[]")
141 |
142 | local ingress_leaf_qdiscs
143 | ingress_leaf_qdiscs=$(get_tc_json tc -s -j qdisc show dev "$ifb_interface")
144 | debug_log "Ingress leaf qdiscs: $ingress_leaf_qdiscs"
145 |
146 | # Process the output to ensure it's valid JSON and filter non-HFSC qdiscs
147 | ingress_leaf_qdiscs=$(echo "$ingress_leaf_qdiscs" | jq -c '[.[] | select(.kind != "hfsc" and .parent != null)]' 2>/dev/null || echo "[]")
148 |
149 | # Get game qdisc type
150 | local gameqdisc
151 | gameqdisc=$(uci -q get qosmate.hfsc.gameqdisc || echo "pfifo")
152 |
153 | # Extract the main HFSC qdisc objects for UI compatibility
154 | local hfsc_egress=$(echo "$hfsc_egress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
155 | local hfsc_ingress=$(echo "$hfsc_ingress_qdisc" | jq -c '.[0] // {}' 2>/dev/null || echo "{}")
156 |
157 | # Create final JSON output using jq to ensure proper escaping
158 | result=$(jq -n \
159 | --arg rq "$root_qdisc" \
160 | --arg wi "$wan_interface" \
161 | --arg ii "$ifb_interface" \
162 | --arg gq "$gameqdisc" \
163 | --argjson fs "$filtered_stats" \
164 | --argjson he "$hfsc_egress" \
165 | --argjson hi "$hfsc_ingress" \
166 | --argjson hec "$hfsc_egress_classes" \
167 | --argjson hic "$hfsc_ingress_classes" \
168 | --argjson elq "$egress_leaf_qdiscs" \
169 | --argjson ilq "$ingress_leaf_qdiscs" \
170 | '{
171 | root_qdisc: $rq,
172 | wan_interface: $wi,
173 | ifb_interface: $ii,
174 | gameqdisc: $gq,
175 | qdisc_stats: $fs,
176 | hfsc_egress: $he,
177 | hfsc_ingress: $hi,
178 | hfsc_egress_classes: $hec,
179 | hfsc_ingress_classes: $hic,
180 | egress_leaf_qdiscs: $elq,
181 | ingress_leaf_qdiscs: $ilq
182 | }' 2>/dev/null)
183 | fi
184 |
185 | echo "$result"
186 | }
187 |
188 | # Function to get historical statistics
189 | get_historical_stats() {
190 | # Note: This is currently a placeholder function for future implementation
191 | # In the future, this could provide historical QoS statistics from a database or log files
192 | # Currently returns an empty JSON object as no history is being saved
193 | local history_file="/tmp/qosmate_stats_history.json"
194 |
195 | if [ -f "$history_file" ]; then
196 | cat "$history_file"
197 | else
198 | echo "{}"
199 | fi
200 | }
201 |
202 | # Function to get RRD data if available
203 | get_rrd_data() {
204 | # Note: This is currently a placeholder function for future implementation
205 | # In the future, this could provide Round Robin Database data for generating time-series charts
206 | # Currently only checks if the RRD directory exists but doesn't actually process any data
207 | # Check if RRD data is available for QoS
208 | if [ -d "/tmp/rrd" ]; then
209 | echo "{\"rrd_available\": true}"
210 | else
211 | echo "{\"rrd_available\": false}"
212 | fi
213 | }
214 |
215 | # Function to handle the call command
216 | handle_call() {
217 | # Process input JSON
218 | local input_json
219 | input_json=$(cat)
220 | debug_log "Input JSON: $input_json"
221 |
222 | # Extract method directly from the JSON
223 | local method
224 | method=$(echo "$input_json" | jsonfilter -e '@.method')
225 | debug_log "Extracted method: $method"
226 |
227 | # Handle empty method case
228 | if [ -z "$method" ]; then
229 | # Default to getStats if no method is specified
230 | method="getStats"
231 | debug_log "Using default method: $method"
232 | fi
233 |
234 | # Process based on method
235 | case "$method" in
236 | getStats)
237 | get_stats
238 | ;;
239 | getHistoricalStats)
240 | get_historical_stats
241 | ;;
242 | getRrdData)
243 | get_rrd_data
244 | ;;
245 | *)
246 | debug_log "Unknown method: $method"
247 | echo '{"error": "Method not found"}'
248 | ;;
249 | esac
250 | }
251 |
252 | # Main function to handle RPC calls
253 | case "$1" in
254 | list)
255 | # List available methods
256 | echo '{"getStats": {}, "getHistoricalStats": {}, "getRrdData": {}}'
257 | ;;
258 | call)
259 | handle_call
260 | ;;
261 | *)
262 | echo '{"error": "Invalid call"}'
263 | ;;
264 | esac
265 |
--------------------------------------------------------------------------------
/root/usr/share/luci/menu.d/luci-app-qosmate.json:
--------------------------------------------------------------------------------
1 | {
2 | "admin/network/qosmate": {
3 | "title": "QoSmate",
4 | "order": 70,
5 | "action": {
6 | "type": "firstchild"
7 | },
8 | "depends": {
9 | "acl": [ "luci-app-qosmate" ],
10 | "uci": { "qosmate": true }
11 | }
12 | },
13 | "admin/network/qosmate/settings": {
14 | "title": "Settings",
15 | "order": 10,
16 | "action": {
17 | "type": "view",
18 | "path": "qosmate/settings"
19 | }
20 | },
21 | "admin/network/qosmate/hfsc": {
22 | "title": "HFSC",
23 | "order": 20,
24 | "action": {
25 | "type": "view",
26 | "path": "qosmate/hfsc"
27 | }
28 | },
29 | "admin/network/qosmate/cake": {
30 | "title": "CAKE",
31 | "order": 30,
32 | "action": {
33 | "type": "view",
34 | "path": "qosmate/cake"
35 | }
36 | },
37 | "admin/network/qosmate/advanced": {
38 | "title": "Advanced",
39 | "order": 40,
40 | "action": {
41 | "type": "view",
42 | "path": "qosmate/advanced"
43 | }
44 | },
45 | "admin/network/qosmate/rules": {
46 | "title": "Rules",
47 | "order": 50,
48 | "action": {
49 | "type": "view",
50 | "path": "qosmate/rules"
51 | }
52 | },
53 | "admin/network/qosmate/ipsets": {
54 | "title": "IP Sets",
55 | "order": 45,
56 | "action": {
57 | "type": "view",
58 | "path": "qosmate/ipsets"
59 | }
60 | },
61 | "admin/network/qosmate/custom_rules": {
62 | "title": "Custom Rules",
63 | "order": 60,
64 | "action": {
65 | "type": "view",
66 | "path": "qosmate/custom_rules"
67 | }
68 | },
69 | "admin/network/qosmate/connections": {
70 | "title": "Connections",
71 | "order": 70,
72 | "action": {
73 | "type": "view",
74 | "path": "qosmate/connections"
75 | }
76 | },
77 | "admin/network/qosmate/statistics": {
78 | "title": "Statistics",
79 | "order": 80,
80 | "action": {
81 | "type": "view",
82 | "path": "qosmate/statistics"
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/root/usr/share/rpcd/acl.d/luci-app-qosmate.json:
--------------------------------------------------------------------------------
1 | {
2 | "luci-app-qosmate": {
3 | "description": "Grant access to QoSmate configuration",
4 | "read": {
5 | "ubus": {
6 | "luci.qosmate": [ "getConntrackDSCP" ],
7 | "luci.qosmate_stats": [ "getStats", "getHistoricalStats", "getRrdData" ],
8 | "luci": [ "getInitList", "setInitAction", "getInitActionStatus", "exec" ]
9 | },
10 | "uci": [ "qosmate" ],
11 | "file": {
12 | "/etc/qosmate.sh": [ "read" ],
13 | "/etc/init.d/qosmate": [ "read", "exec" ],
14 | "/etc/hotplug.d/iface/13-qosmateHotplug": [ "read" ],
15 | "/etc/config/qosmate": [ "read" ],
16 | "/tmp/qosmate_auto_setup_output.txt": [ "read" ],
17 | "/etc/qosmate.d/custom_rules.nft": [ "read" ],
18 | "/tmp/qosmate_custom_rules_validation.txt": [ "read" ],
19 | "/tmp/qosmate_stats_history.json": [ "read" ]
20 | }
21 | },
22 | "write": {
23 | "ubus": {
24 | "luci": [ "setInitAction", "exec" ]
25 | },
26 | "uci": [ "qosmate" ],
27 | "file": {
28 | "/etc/qosmate.sh": [ "write" ],
29 | "/etc/init.d/qosmate": [ "write" ],
30 | "/etc/hotplug.d/iface/13-qosmateHotplug": [ "write" ],
31 | "/etc/qosmate.d/custom_rules.nft": [ "write" ]
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------