🚀 Sync 2026-03-08 00:54:34

This commit is contained in:
github-actions[bot] committed 2026-03-08 00:54:34 +08:00
1 parent 97d524986e
commit 5997aee442
22 files changed
+3954 -28

No files matched your search

@@ -0,0 +1,165 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require form';
'require poll';
return view.extend({
render: function() {
var state = {
running: false,
port: 3300
};
var container = E('div');
var statusSection = E('div', { 'class': 'cbi-section' });
var statusIcon = E('span', { 'style': 'margin-right: 5px;' });
var statusText = E('span');
var toggleBtn = E('button', { 'class': 'btn cbi-button' });
var statusMessage = E('div', { style: 'text-align: center; padding: 2em;' }, [
E('img', {
src: 'data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMjQiIGhlaWdodD0iMTAyNCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCI+PHBhdGggZmlsbD0iI2RmMDAwMCIgZD0iTTk0Mi40MjEgMjM0LjYyNGw4MC44MTEtODAuODExLTE1My4wNDUtMTUzLjA0NS04MC44MTEgODAuODExYy03OS45NTctNTEuNjI3LTE3NS4xNDctODEuNTc5LTI3Ny4zNzYtODEuNTc5LTI4Mi43NTIgMC01MTIgMjI5LjI0OC01MTIgNTEyIDAgMTAyLjIyOSAyOS45NTIgMTk3LjQxOSA4MS41NzkgMjc3LjM3NmwtODAuODExIDgwLjgxMSAxNTMuMDQ1IDE1My4wNDUgODAuODExLTgwLjgxMWM3OS45NTcgNTEuNjI3IDE3NS4xNDcgODEuNTc5IDI3Ny4zNzYgODEuNTc5IDI4Mi43NTIgMCA1MTItMjI5LjI0OCA1MTItNTEyIDAtMTAyLjIyOS0yOS45NTItMTk3LjQxOS04MS41NzktMjc3LjM3NnpNMTk0Ljk0NCA1MTJjMC0xNzUuMTA0IDE0MS45NTItMzE3LjA1NiAzMTcuMDU2LTMxNy4wNTYgNDggMCA5My40ODMgMTAuNjY3IDEzNC4yMjkgMjkuNzgxbC00MjEuNTQ3IDQyMS41NDdjLTE5LjA3Mi00MC43ODktMjkuNzM5LTg2LjI3Mi0yOS43MzktMTM0LjI3MnpNNTEyIDgyOS4wNTZjLTQ4IDAtOTMuNDgzLTEwLjY2Ny0xMzQuMjI5LTI5Ljc4MWw0MjEuNTQ3LTQyMS41NDdjMTkuMDcyIDQwLjc4OSAyOS43ODEgODYuMjcyIDI5Ljc4MSAxMzQuMjI5LTAuMDQzIDE3NS4xNDctMTQxLjk5NSAzMTcuMDk5LTMxNy4wOTkgMzE3LjA5OXoiLz48L3N2Zz4=',
style: 'width: 100px; height: 100px; margin-bottom: 1em;'
}),
E('h2', {}, _('Homebox Service Not Running')),
E('p', {}, _('Please enable the Homebox service'))
]);
var isHttps = window.location.protocol === 'https:';
var iframe;
if (!isHttps) {
iframe = E('iframe', {
src: window.location.origin + ':' + state.port,
style: 'border:none;width: 100%; min-height: 80vh; border: none; border-radius: 3px;overflow:hidden !important;'
});
}
function createHttpsButton() {
return E('div', {
style: 'text-align: center; padding: 2em;'
}, [
E('h2', {}, _('Homebox Control panel')),
E('p', {}, _('Due to browser security policies, the Homebox interface https cannot be embedded directly.')),
E('a', {
href: 'http://' + window.location.hostname + ':' + state.port,
target: '_blank',
class: 'cbi-button cbi-button-apply',
style: 'display: inline-block; margin-top: 1em; padding: 10px 20px; font-size: 16px; text-decoration: none; color: white;'
}, _('Open Web Interface'))
]);
}
async function checkProcess() {
try {
// 尝试使用pgrep
const res = await fs.exec('/usr/bin/pgrep', ['homebox']);
return {
running: res.code === 0,
pid: res.stdout.trim() || null
};
} catch (err) {
// 回退到ps方法
try {
const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'homebox', '-o', 'pid=']);
const pid = psRes.stdout.trim();
return {
running: pid !== '',
pid: pid || null
};
} catch (err) {
return { running: false, pid: null };
}
}
}
function controlService(action) {
var command = action === 'start'
? 'nohup /usr/bin/homebox >> /tmp/netspeedtest.log 2>&1 &'
: '/usr/bin/killall homebox';
return fs.exec('/bin/sh', ['-c', command]);
}
function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red';
statusText.textContent = _('Homebox Server') + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.style.color = state.running ? 'green' : 'red';
statusText.style.fontWeight = 'bold';
statusText.style.fontSize = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
// Update container content based on state and protocol
container.textContent = '';
if (state.running) {
if (isHttps) {
container.appendChild(createHttpsButton());
} else {
container.appendChild(iframe);
}
} else {
container.appendChild(statusMessage);
}
}
toggleBtn.addEventListener('click', ui.createHandlerFn(this, function() {
var action = state.running ? 'stop' : 'start';
return controlService(action)
.then(checkProcess)
.then(res => {
state.running = res.running;
updateStatus();
});
}));
statusSection.appendChild(E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Homebox')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Homebox service control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]));
// Initial status check
checkProcess().then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
// Start polling
poll.add(() => {
return checkProcess().then(res => {
if (res.running !== state.running) {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
}
});
}, 5);
poll.start();
});
return E('div', {}, [
statusSection,
container
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,168 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require form';
'require poll';
var state = {
running: false,
port: null
};
const logPath = '/tmp/netspeedtest.log';
async function checkProcess() {
try {
const res = await fs.exec('/usr/bin/pgrep', ['iperf3']);
if (res.code === 0 && res.stdout.trim()) {
return { running: true, pid: res.stdout.trim() };
}
// 回退检查
const psRes = await fs.exec('/bin/ps', ['-w', '-C', 'iperf3', '-o', 'pid=']);
const pid = psRes.stdout.trim();
return {
running: !!pid,
pid: pid || null
};
} catch (err) {
console.error('Process check error:', err);
return { running: false, pid: null };
}
}
function controlService(action) {
const commands = {
start: `/usr/bin/iperf3 -s -D -p 5201 --logfile ${logPath} 2>&1`,
stop: '/usr/bin/killall -q iperf3'
};
return (action === 'start'
? fs.exec('/bin/sh', ['-c', `mkdir -p /tmp/netspeedtest && touch ${logPath} && chmod 644 ${logPath}`])
: Promise.resolve()
).then(() => fs.exec('/bin/sh', ['-c', commands[action]]))
.catch(err => {
console.error('Service control error:', err);
throw err;
});
}
return view.extend({
handleSaveApply: null,
handleSave: null,
handleReset: null,
load: function() {
return Promise.all([
uci.load('netspeedtest')
]);
},
render: function() {
// 创建状态元素
const statusIcon = E('span', { 'style': 'margin-right: 5px;' });
const btnGroup = E('div', { 'class': 'cbi-value-field', 'style': 'display: flex; gap: 10px;' });
const statusText = E('span');
const toggleBtn = E('button', {
'class': 'btn cbi-button',
'click': ui.createHandlerFn(this, function() {
const action = state.running ? 'stop' : 'start';
toggleBtn.disabled = true; // 禁用按钮
return controlService(action)
.then(() => checkProcess())
.then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false; // 恢复按钮
})
.catch(err => {
ui.addNotification(null, E('p', _('Error: ') + err.message), 'error');
toggleBtn.disabled = false; // 出错时也要恢复按钮
});
})
});
function updateStatus() {
statusIcon.textContent = state.running ? '✓' : '✗';
statusIcon.style.color = state.running ? 'green' : 'red';
statusText.textContent = _('Iperf3 Server ') + (state.running ? _('RUNNING') : _('NOT RUNNING'));
statusText.style.color = state.running ? 'green' : 'red';
statusText.style['font-weight'] = 'bold';
statusText.style['font-size'] = '0.92rem';
toggleBtn.textContent = state.running ? _('Stop Server') : _('Start Server');
toggleBtn.className = `btn cbi-button cbi-button-${state.running ? 'reset' : 'apply'}`;
}
// 初始化状态
statusIcon.textContent = '...';
statusText.textContent = _('Checking status...');
toggleBtn.textContent = _('Loading...');
toggleBtn.disabled = true;
// 构建UI
const statusSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'margin: 15px' }, [
E('h3', {}, _('Throughput speedtest Iperf3')),
E('div', { 'class': 'cbi-map-descr' }, [statusIcon, statusText]),
E('div', {'class': 'cbi-value', 'style': 'margin-top: 20px'}, [
E('div', {'class': 'cbi-value-title'}, _('Iperf3 service control')),
E('div', {'class': 'cbi-value-field'}, toggleBtn),
E('div', {'class': 'cbi-value-title'}, _('Download iperf3 client')),
E('div', {'class': 'cbi-value-field'}, [
E('div', {
'class': 'cbi-value-field',
'style': 'display: flex;'
}, [
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://iperf.fr/iperf-download.php', '_blank'))
}, _('Official Website')),
E('button', {
'class': 'btn cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, () => window.open('https://github.com/sirpdboy/luci-app-netspeedtest/releases', '_blank'))
}, _('GitHub'))
])
])
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 20px;' }, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy/luci-app-netspeedtest',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
// 初始化状态检查
checkProcess().then(res => {
state.running = res.running;
updateStatus();
toggleBtn.disabled = false;
// 启动轮询
poll.add(() => {
return checkProcess().then(res => {
if (res.running !== state.running) {
state.running = res.running;
updateStatus();
}
});
}, 5);
});
return statusSection;
}
});
@@ -0,0 +1,98 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require dom';
'require fs';
'require poll';
'require uci';
'require view';
var scrollPosition = 0;
var userScrolled = false;
var logTextarea;
var log_path;
uci.load('netspeedtest').then(function() {
log_path = '/tmp/netspeedtest.log';
});
function pollLog() {
return Promise.all([
fs.read_direct(log_path, 'text').then(function(res) {
return res.trim()
.split(/\n/).join('\n')
.replace(/\u001b\[33mWARN\u001b\[0m/g, '')
.replace(/\u001b\[36mINFO\u001b\[0m/g, '')
.replace(/\u001b\[31mERRO\u001b\[0m/g, '');
}),
]).then(function(data) {
logTextarea.value = data[0] || _('No log data.');
if (!userScrolled) {
logTextarea.scrollTop = logTextarea.scrollHeight;
} else {
logTextarea.scrollTop = scrollPosition;
}
});
}
return view.extend({
handleCleanLogs: function() {
return fs.write(log_path, '')
.catch(function(e) {
ui.addNotification(null, E('p', e.message))
});
},
render: function() {
logTextarea = E('textarea', {
'class': 'cbi-input-textarea',
'wrap': 'off',
'readonly': 'readonly',
'style': 'width: calc(100% - 20px); height: 535px; margin: 10px; overflow-y: scroll;'
});
logTextarea.addEventListener('scroll', function() {
userScrolled = true;
scrollPosition = logTextarea.scrollTop;
});
var log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
setTimeout(function() {
poll.add(pollLog);
}, 100);
var clear_logs_button = E('input', {
'class': 'btn cbi-button-action',
'type': 'button',
'style': 'margin-left: 20px; margin-top: 10px;',
'value': _('Clear logs')
});
clear_logs_button.addEventListener('click', this.handleCleanLogs.bind(this));
return E('div', { 'class': 'cbi-map' }, [
E('div', { 'class': 'cbi-section' }, [
clear_logs_button,
log_textarea_wrapper,
E('div', { 'style': 'text-align: right' }, [
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
]),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
E('div', { 'style': 'text-align: right; font-style: italic; margin-top: 10px;' }, [
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
}
// handleSaveApply: null,
// handleSave: null,
// handleReset: null
});
@@ -0,0 +1,193 @@
/* Copyright (C) 2021-2026 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-netspeedtest */
'use strict';
'require view';
'require uci';
'require form';
return view.extend({
load() {
return Promise.all([
uci.load('netspeedtest')
]);
},
render(res) {
let m, s, o;
m = new form.Map('netspeedtest', _('Online SpeedTest'));
let currentSpeedTestUrl = uci.get('netspeedtest', 'config', 'speedtest_site') || 'https://plugin.speedtest.cn/taste#/?t=1767018285493';
if (!currentSpeedTestUrl) {
currentSpeedTestUrl = 'https://plugin.speedtest.cn/taste#/?t=1767018285493';
uci.set('netspeedtest', 'config', 'speedtest_site', currentSpeedTestUrl);
uci.save();
}
s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true;
// 下拉选择
o = s.option(form.DummyValue, '_speedtest_select');
o.textvalue = function() {
return '';
};
o.render = function(section_id) {
const sites = [
{url: 'https://plugin.speedtest.cn/taste#/?t=1767018285493', name: 'speedtest.cn'},
{url: 'https://static.hdslb.com/', name: 'hdslb.com'},
{url: 'https://test.ustc.edu.cn/', name: 'ustc.edu.cn'},
{url: 'https://www1.szu.edu.cn/nc/speedtest/', name: 'szu.edu.cn'},
{url: 'https://10000.gd.cn/#/speed', name: 'gd.cn'},
{url: 'https://speed.cloudflare.com/', name: 'cloudflare.com'},
{url: 'https://fast.com/', name: 'Netflix fast.com'},
{url: '//openspeedtest.com/speedtest', name: 'openspeedtest.com'}
];
const container = E('div', {
class: 'cbi-value'
});
const label = E('label', {
class: 'cbi-value-title',
for: 'speedtest-site-select'
}, _('Select speed measurement station'));
const field = E('div', {
class: 'cbi-value-field',
});
const select = E('select', {
id: 'speedtest-site-select',
class: 'cbi-input-select'
});
sites.forEach(site => {
const option = E('option', {
value: site.url,
selected: currentSpeedTestUrl === site.url ? 'selected' : null
}, site.name);
select.appendChild(option);
});
function saveConfig(url) {
try {
uci.set('netspeedtest', 'config', 'speedtest_site', url);
uci.save();
} catch (e) {
}
}
select.addEventListener('change', function() {
const iframe = document.getElementById('speedtest-iframe');
if (iframe && this.value) {
currentSpeedTestUrl = this.value;
iframe.src = this.value;
saveConfig(this.value);
}
});
field.appendChild(select);
container.appendChild(label);
container.appendChild(field);
const saveStatus = E('small', {
id: 'save-status',
style: 'display: block; margin-top: 5px; color: #28a745; opacity: 0; transition: opacity 0.3s;'
});
field.appendChild(saveStatus);
return container;
};
s = m.section(form.NamedSection, '_iframe');
s.anonymous = true;
s.render = function(section_id) {
const container = E('div', {
class: 'speedtest-wrapper mobiliframe',
style: 'width:100%;height:550px;position:relative;overflow:hidden;margin-top:20px;'
});
const header = E('div', {
style: 'display:flex;justify-content:space-between;align-items:center;padding: 0.5rem 1rem;'
}, [
E('span', {
style: 'font-weight:bold;'
}, _('NetSpeedtest')),
]);
const iframeContainer = E('div', {
style: 'height: calc(100% - 33px);'
});
const iframe = E('iframe', {
id: 'speedtest-iframe',
src: currentSpeedTestUrl,
style: 'width:100%;height:100%;border:none;background: #fff'
});
iframeContainer.appendChild(iframe);
const style = E('style', {}, `
.speedtest-wrapper {
transition: all 0.3s ease;
}
.speedtest-wrapper:hover {
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
}
#save-status {
transition: opacity 0.3s ease;
}
@media (prefers-color-scheme: dark) {
.speedtest-wrapper {
background: #2d2d2d;
}
.speedtest-wrapper > div:first-child {
background: #3d3d3d;
border-color: #555;
}
.speedtest-wrapper > div:first-child span {
color: #e0e0e0;
}
}
@media (max-width: 768px) {
.speedtest-wrapper {
height: 500px;
}
.cbi-value-title {
width: 100% !important;
margin-bottom: 5px;
}
.cbi-value-field {
width: 100% !important;
}
#speedtest-site-select {
max-width: 100%;
}
}
`);
container.appendChild(header);
container.appendChild(iframeContainer);
container.appendChild(style);
return container;
};
return m.render();
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,481 @@
// Copyright (C) 2019-2026 sirpdboy
// Fixed version - better Ookla result parsing
'use strict';
'require view';
'require poll';
'require dom';
'require fs';
'require uci';
'require ui';
'require form';
var Timeout = 300 * 1000;
var ResultFile = '/tmp/netspeedtest_result';
var SpeedtestScript = '/usr/bin/netspeedtest.sh';
return view.extend({
load() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
L.resolveDefault(fs.stat('/usr/bin/speedtest'), {}),
L.resolveDefault(fs.read(ResultFile), null),
L.resolveDefault(fs.stat(ResultFile), {}),
uci.load('netspeedtest')
]);
},
// 检测可用版本
detectVersions: function(res) {
var hasOokla = !!(res[0] && res[0].path);
var hasPython = !!(res[1] && res[1].path);
return {
ookla: hasOokla,
python: hasPython,
available: hasOokla || hasPython
};
},
// 检查是否真正在测试中(文件新鲜且内容为Testing)
isTesting: function(resultContent, resultMtime) {
if (!resultContent || resultContent.length === 0) return false;
if (resultContent[0].trim() !== 'Testing') return false;
// 检查文件是否在超时时间内
var fileAge = Date.now() - resultMtime;
return fileAge < Timeout;
},
// 解析结果内容
parseResult: function(content) {
if (!content || content.length === 0) {
return { type: 'none', data: null };
}
var firstLine = content[0].trim();
// 检查是否是Testing状态
if (firstLine === 'Testing') {
return { type: 'testing', data: null };
}
// 检查是否是失败状态
if (firstLine === 'Test failed') {
return { type: 'failed', data: null };
}
// 检查是否包含Result URL (Ookla格式)
var resultUrl = null;
for (var i = 0; i < content.length; i++) {
var line = content[i];
if (line.match(/Result URL:/i)) {
var urlMatch = line.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
resultUrl = urlMatch[0];
break;
}
}
}
if (resultUrl) {
return { type: 'url', data: resultUrl };
}
// 检查是否是直接URL (Python格式)
if (firstLine.match(/^https?:\/\//) || firstLine.match(/\.png$/)) {
return { type: 'url', data: firstLine };
}
// 检查是否是速度数据
if (firstLine.match(/Download:/i) || firstLine.match(/Upload:/i)) {
return { type: 'speed', data: content.join('\n') };
}
// 其他情况,可能是完整的速度测试结果
// 检查是否包含Download和Upload信息
var hasDownload = false;
var hasUpload = false;
for (var j = 0; j < content.length; j++) {
if (content[j].match(/Download:/i)) hasDownload = true;
if (content[j].match(/Upload:/i)) hasUpload = true;
}
if (hasDownload && hasUpload) {
return { type: 'speed', data: content.join('\n') };
}
return { type: 'unknown', data: content.join('\n') };
},
poll_status(nodes, res) {
var result_content = res[2] ? res[2].trim().split("\n") : [];
var result_mtime = res[3] ? res[3].mtime * 1000 : 0;
var result_stat = nodes.querySelector('#speedtest_result');
var start_btn = nodes.querySelector('.cbi-button-apply');
// 解析结果
var result = this.parseResult(result_content);
var is_testing = this.isTesting(result_content, result_mtime);
// 更新测试按钮状态
if (start_btn) {
start_btn.disabled = is_testing;
}
// 更新结果状态
if (result_stat) {
if (is_testing) {
result_stat.innerHTML = "<span style='color:green;font-weight:bold;margin-left:20px'>" +
"<img src='/luci-static/resources/icons/loading.svg' height='17' style='vertical-align:middle; margin-right:5px'/> " +
_('Speed testing in progress...') +
"</span>";
return;
}
switch(result.type) {
case 'failed':
result_stat.innerHTML = "<span style='color:red;font-weight:bold;margin-left:20px'>" +
"⚠️ " + _('Test failed. Please check log for details.') +
"</span>";
break;
case 'url':
var imageUrl = result.data;
if (!imageUrl.match(/\.png$/)) {
imageUrl = imageUrl + '.png';
}
// 处理speedtest.net的特殊URL
if (imageUrl.includes('speedtest.net/result/')) {
imageUrl = imageUrl.replace('/result/c/', '/result/');
}
result_stat.innerHTML = "<div style='max-width:500px; margin-left:20px'>" +
"<a href='" + result.data + "' target='_blank'>" +
"<img src='" + result.data + "' style='max-width:100%; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1)' " +
"onerror='this.onerror=null; this.src=\"/luci-static/resources/icons/error.png\"'/>" +
"</a><br>" +
"</div>";
break;
case 'speed':
var lines = result.data.split('\n');
var download = '', upload = '', latency = '', packetLoss = '', server = '', isp = '';
lines.forEach(function(line) {
if (line.match(/Download:/i)) download = line;
else if (line.match(/Upload:/i)) upload = line;
else if (line.match(/Latency:/i) || line.match(/Idle Latency:/i)) latency = line;
else if (line.match(/Packet Loss:/i)) packetLoss = line;
else if (line.match(/Server:/i)) server = line;
else if (line.match(/ISP:/i)) isp = line;
});
var html = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>";
html += "<strong>" + _('Test Results:') + "</strong>";
if (server) {
html += "<div style='margin-top:5px; padding:5px; color:#666'>" +
server.trim() + "</div>";
}
if (download) {
html += "<div style='margin-top:10px; padding:8px; background:#e8f5e8; border-radius:4px'>" +
"<span style='font-weight:bold'>⬇️ " + _('Download:') + "</span> " +
"<span style='color:#2e7d32; font-weight:500'>" + download.replace(/Download:/i, '').trim() + "</span>" +
"</div>";
}
if (upload) {
html += "<div style='margin-top:5px; padding:8px; background:#e3f2fd; border-radius:4px'>" +
"<span style='font-weight:bold'>⬆️ " + _('Upload:') + "</span> " +
"<span style='color:#1565c0; font-weight:500'>" + upload.replace(/Upload:/i, '').trim() + "</span>" +
"</div>";
}
if (latency) {
html += "<div style='margin-top:5px; padding:5px; background:#f5f5f5; border-radius:4px'>" +
"<span style='font-weight:bold'>⏱️ " + _('Latency:') + "</span> " +
"<span style='color:#666'>" + latency.replace(/Latency:/i, '').replace(/Idle Latency:/i, '').trim() + "</span>" +
"</div>";
}
if (packetLoss) {
html += "<div style='margin-top:5px; padding:5px; color:#666'>" +
packetLoss.trim() + "</div>";
}
html += "</div>";
result_stat.innerHTML = html;
break;
case 'unknown':
// 未知格式,直接显示
result_stat.innerHTML = "<div style='margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px'>" +
"<strong>" + _('Test Results:') + "</strong><br>" +
"<pre style='margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'>" +
escapeHTML(result.data) + "</pre>" +
"</div>";
break;
default:
// 无结果
result_stat.innerHTML = "<span style='color:gray;margin-left:20px'>" +
"<em>" + _('No test results yet. Click "Start Speed Test" to begin.') + "</em>" +
"</span>";
}
}
},
render(res) {
var self = this;
var has_ookla = !!(res[0] && res[0].path);
var has_python = !!(res[1] && res[1].path);
var result_content = res[2] ? res[2].trim().split("\n") : [];
var result_mtime = res[3] ? res[3].mtime * 1000 : 0;
var versions = this.detectVersions(res);
var result = this.parseResult(result_content);
var is_testing = this.isTesting(result_content, result_mtime);
var m, s, o;
m = new form.Map('netspeedtest', _('WAN SpeedTest'));
// 结果显示区域
s = m.section(form.TypedSection, '_result');
s.anonymous = true;
s.render = function(section_id) {
var result_id = 'speedtest_result';
if (is_testing) {
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:green;font-weight:bold;margin-left:20px' }, [
E('img', {
src: '/luci-static/resources/icons/loading.svg',
height: '17',
style: 'vertical-align:middle; margin-right:5px'
}),
_('Speed testing in progress...')
])
]);
}
switch(result.type) {
case 'failed':
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:red;font-weight:bold;margin-left:20px' }, [
'⚠️ ' + _('Test failed. Please check log for details.')
])
]);
case 'url':
var imageUrl = result.data;
if (!imageUrl.match(/\.png$/)) {
imageUrl = imageUrl + '.png';
}
if (imageUrl.includes('speedtest.net/result/')) {
imageUrl = imageUrl.replace('/result/c/', '/result/');
}
return E('div', { id: result_id, class: 'cbi-section' }, [
E('div', { style: 'max-width:500px; margin-left:20px' }, [
E('a', { href: result.data, target: '_blank' }, [
E('img', {
src: imageUrl,
style: 'max-width:100%; border-radius:4px; box-shadow:0 2px 4px rgba(0,0,0,0.1)'
})
]),
E('br'),
E('small', {}, [
E('a', {
href: result.data,
target: '_blank',
style: 'display:inline-block; margin-top:5px; color:#0066cc'
}, _('View detailed results'))
])
])
]);
case 'speed':
var lines = result.data.split('\n');
var download = '', upload = '', latency = '';
lines.forEach(function(line) {
if (line.match(/Download:/i)) download = line;
else if (line.match(/Upload:/i)) upload = line;
else if (line.match(/Latency:/i) || line.match(/Idle Latency:/i)) latency = line;
});
var children = [
E('strong', {}, _('Test Results:'))
];
if (download) {
children.push(
E('div', {
style: 'margin-top:10px; padding:8px; background:#e8f5e8; border-radius:4px'
}, [
E('span', { style: 'font-weight:bold' }, '⬇️ ' + _('Download:') + ' '),
E('span', { style: 'color:#2e7d32; font-weight:500' }, download.replace(/Download:/i, '').trim())
])
);
}
if (upload) {
children.push(
E('div', {
style: 'margin-top:5px; padding:8px; background:#e3f2fd; border-radius:4px'
}, [
E('span', { style: 'font-weight:bold' }, '⬆️ ' + _('Upload:') + ' '),
E('span', { style: 'color:#1565c0; font-weight:500' }, upload.replace(/Upload:/i, '').trim())
])
);
}
if (latency) {
children.push(
E('div', {
style: 'margin-top:5px; padding:5px; background:#f5f5f5; border-radius:4px'
}, [
E('span', { style: 'font-weight:bold' }, '⏱️ ' + _('Latency:') + ' '),
E('span', { style: 'color:#666' }, latency.replace(/Latency:/i, '').replace(/Idle Latency:/i, '').trim())
])
);
}
return E('div', {
id: result_id,
class: 'cbi-section'
}, [
E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, children)
]);
case 'unknown':
// 未知格式,直接显示
return E('div', { id: result_id, class: 'cbi-section' }, [
E('div', { style: 'margin-left:20px; padding:10px; background:#f5f5f5; border-radius:4px' }, [
E('strong', {}, _('Test Results:')),
E('br'),
E('pre', {
style: 'margin:5px 0 0 0; font-family:inherit; white-space:pre-wrap; color:#2e7d32'
}, escapeHTML(result.data))
])
]);
default:
// 无结果
return E('div', { id: result_id, class: 'cbi-section' }, [
E('span', { style: 'color:gray;margin-left:20px' }, [
E('em', {}, _('No test results yet. Click "Start Speed Test" to begin.'))
])
]);
}
};
// 配置部分
s = m.section(form.NamedSection, 'config', 'netspeedtest');
s.anonymous = true;
// 版本选择
o = s.option(form.ListValue, 'test_version', _('Select Test Version'));
if (has_ookla) {
o.value('ookla', 'Ookla SpeedTest');
}
if (has_python) {
o.value('python', 'Python speedtest-cli');
}
if (has_ookla) {
o.default = 'ookla';
} else if (has_python) {
o.default = 'python';
}
o.write = function(section_id, formvalue) {
uci.set('netspeedtest', section_id, 'test_version', formvalue);
return uci.save('netspeedtest');
};
// 开始测试按钮
o = s.option(form.Button, 'start', _('Start Speed Test'));
o.inputtitle = _('Click to start speed test');
o.inputstyle = 'apply';
// 只有真正在测试中才禁用按钮
if (is_testing) {
o.readonly = true;
}
o.onclick = function() {
var btn = this;
btn.disabled = true;
// 获取选中的版本
var versionSelect = document.getElementById('widget.cbid.netspeedtest.config.test_version');
if (!versionSelect) {
versionSelect = document.querySelector('select[name="test_version"]');
}
var version = versionSelect ? versionSelect.value : (has_ookla ? 'ookla' : 'python');
// 先写入Testing状态
return fs.write(ResultFile, 'Testing\n')
.then(function() {
// 在后台执行测试脚本
var cmd = 'nohup ' + SpeedtestScript;
if (version) {
cmd += ' --version ' + version;
}
cmd += ' > /dev/null 2>&1 &';
return fs.exec('/bin/sh', ['-c', cmd]);
})
.then(function() {
ui.addNotification(null, E('p', _('Speed test started. Please wait...')), 'info');
})
.catch(function(e) {
console.error('Failed to start test:', e);
ui.addNotification(null, E('p', _('Failed to start speed test: ') + e.message), 'error');
});
return false;
};
return m.render()
.then(L.bind(function(m, nodes) {
nodes.result_mtime = result_mtime; // 保存结果文件修改时间
// 添加轮询 - 每2秒检查一次结果
poll.add(L.bind(function() {
return Promise.all([
L.resolveDefault(fs.stat('/usr/bin/ookla-speedtest'), {}),
L.resolveDefault(fs.stat('/usr/bin/speedtest'), {}),
L.resolveDefault(fs.read(ResultFile), null),
L.resolveDefault(fs.stat(ResultFile), {})
]).then(L.bind(function(res) {
nodes.result_mtime = res[3] ? res[3].mtime * 1000 : 0;
this.poll_status(nodes, res);
}, this));
}, this), 2); // 2秒轮询
return nodes;
}, this, m));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
// 辅助函数
function escapeHTML(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}