luci-app-ocserv: fix status page and users view bugs

- Fix duplicate "Active OpenConnect Users" header on status page by
  returning the table directly like other status widgets
- Fix colspan mismatch in users table (13 columns, was 10)
- Fix undefined tx/rx in user list by mapping occtl JSON fields
- Switch status widget to occtl --json and add Tx/Rx columns

Fixes: #8439
Signed-off-by: Joshua Klinesmith <joshuaklinesmith@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joshua Klinesmith
2026-03-30 18:09:44 -04:00
committed by Paul Donald
parent 0529dca7a2
commit a5bedae648
2 changed files with 16 additions and 37 deletions

View File

@@ -68,7 +68,9 @@ return L.view.extend({
device: entry?.device,
time: entry?.time || entry['connected-at'],
cipher: entry?.cipher,
status: entry?.status
status: entry?.status,
tx: entry?._TX || entry?.TX || entry?.tx,
rx: entry?._RX || entry?.RX || entry?.rx
};
},
@@ -151,7 +153,7 @@ return L.view.extend({
if (users.length === 0) {
table.appendChild(
E('div', { 'class': 'tr placeholder' }, [
E('div', { 'class': 'td', 'colspan': 10 },
E('div', { 'class': 'td', 'colspan': 13 },
E('em', _('Collecting data...')))
])
);

View File

@@ -39,9 +39,8 @@ return baseclass.extend({
return users;
} catch (e) {
// Fall back to text parsing for non-JSON output
console.warn('JSON parsing failed, falling back to text parsing:', e.message);
return this.parseUsersText(output);
console.error('Failed to parse JSON:', e.message);
return users;
}
},
@@ -56,35 +55,12 @@ return baseclass.extend({
device: entry?.device,
time: entry?.time || entry['connected-at'],
cipher: entry?.cipher,
status: entry?.status
status: entry?.status,
tx: entry?._TX || entry?.TX || entry?.tx,
rx: entry?._RX || entry?.RX || entry?.rx
};
},
parseUsersText: function(output) {
const users = [];
if (!output) return users;
const lines = output.split('\n');
for (let line of lines) {
// Parse: id user group vpn_ip ip device time cipher status
const match = line.match(/^\s*(\d+)\s+([-_\w]+)\s+([().*\-_\w]+)\s+([:.\-_\w]+)\s+([:.\-_\w]+)\s+([:.\-_\w]+)\s+([:.\-_\w]+)\s+([():.\-_\w]+)\s+([:.\-_\w]+)/);
if (match) {
users.push({
id: match[1],
user: match[2],
group: match[3],
vpn_ip: match[4],
ip: match[5],
device: match[6],
time: match[7],
cipher: match[8],
status: match[9]
});
}
}
return users;
},
handleDisconnect: function(id) {
return L.resolveDefault(
fs.exec('/usr/bin/occtl', ['disconnect', 'id', id]),
@@ -100,7 +76,7 @@ return baseclass.extend({
load: function() {
return L.resolveDefault(
fs.exec('/usr/bin/occtl', ['show', 'users']).then(res => res.stdout),
fs.exec('/usr/bin/occtl', ['--json', 'show', 'users']).then(res => res.stdout),
''
);
},
@@ -118,6 +94,8 @@ return baseclass.extend({
E('div', { 'class': 'th' }, _('Time')),
E('div', { 'class': 'th' }, _('Cipher')),
E('div', { 'class': 'th' }, _('Status')),
E('div', { 'class': 'th' }, _('Tx')),
E('div', { 'class': 'th' }, _('Rx')),
E('div', { 'class': 'th' }, '\u00a0')
])
]);
@@ -141,6 +119,8 @@ return baseclass.extend({
E('div', { 'class': 'td' }, user.time),
E('div', { 'class': 'td' }, user.cipher),
E('div', { 'class': 'td' }, user.status),
E('div', { 'class': 'td' }, user.tx),
E('div', { 'class': 'td' }, user.rx),
E('div', { 'class': 'td' },
E('button', {
'class': 'cbi-button cbi-button-remove',
@@ -151,9 +131,6 @@ return baseclass.extend({
}
}
return E('div', { 'class': 'cbi-section' }, [
E('legend', _('Active OpenConnect Users')),
table
]);
return table;
}
});