🐤 Sync 2026-03-30 12:52:58

This commit is contained in:
github-actions[bot] committed 2026-03-30 12:52:58 +08:00
1 parent 238cb2aa39
commit 9941499a61
63 files changed
+3456 -4029

No files matched your search

@@ -1,3 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
/*
* Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
*/
'use strict';
'require view';
'require fs';
@@ -31,13 +36,12 @@ function checkTimeControlProcess() {
}
}
return { running: running, pid: pid };
return { running: running, pid: null };
}).catch(function() {
return { running: false, pid: null };
});
}
// 渲染服务状态显示
function renderServiceStatus(isRunning, pid) {
var statusText = isRunning ? _('RUNNING') : _('NOT RUNNING');
var color = isRunning ? 'green' : 'red';
@@ -114,16 +118,20 @@ return view.extend({
load: function() {
return Promise.all([
uci.load('timecontrol'),
network.getDevices()
network.getHostHints()
]);
},
render: function(data) {
var m, s, o;
var hostList = [];
let hosts = data[1]?.hosts;
m = new form.Map('timecontrol', _('Internet Time Control'),
_('Users can limit their internet usage time through MAC and IP, with available IP ranges such as 192.168.110.00 to 192.168.10.200') + '<br/>' +
_('黑名单模式时间控制方式:') + '<br/>' +
_('1. 时间段控制: 指定的机器在设定时间段内可以上网,其他时间不能上网') + '<br/>' +
_('2. 允许上机时长: 指定的机器上线后可以上网指定时长,超过时长后不能上网') + '<br/>' +
_('3. 组合控制: 在时间段内+时长限制(在允许的时间段内限制上网时长)') + '<br/>' +
_('Suggested feedback:') + ' <a href="https://github.com/sirpdboy/luci-app-timecontrol.git" target="_blank">GitHub @timecontrol</a>');
s = m.section(form.TypedSection);
@@ -171,7 +179,6 @@ return view.extend({
])
]);
};
s = m.section(form.TypedSection, 'timecontrol');
s.anonymous = true;
s.addremove = false;
@@ -202,50 +209,113 @@ return view.extend({
o = s.option(form.Flag, 'enable', _('Enabled'));
o.rmempty = false;
o.default = '1';
o = s.option(form.Value, 'mac', _('IP/MAC Address'));
o.rmempty = false;
o.placeholder = '192.168.10.100 or 00:11:22:33:44:55';
getHostList().then(function(hosts) {
hostList = hosts;
o.value('', _('-- Please select or enter manually --'));
if (hosts) {
var hostOptions = {};
if (hosts.length > 0) {
hosts.forEach(function(host) {
var displayName = '';
if (host.name) {
displayName = host.name + ' - ';
}
displayName += host.ipv4 + ' (' + host.mac + ')';
// 添加IP选项
o.value(host.ipv4, displayName);
// 添加MAC选项
var macDisplay = host.mac;
if (host.name) {
macDisplay += ' - ' + host.name;
}
if (host.ipv4) {
macDisplay += ' (' + host.ipv4 + ')';
}
o.value(host.mac, macDisplay);
});
Object.keys(hosts).forEach(function(mac) {
var host = hosts[mac];
var name = host.name || _(' ');
var ips = L.toArray(host.ipaddrs || host.ipv4 || []);
if (ips.length > 0) {
ips.forEach(function(ip) {
var macDisplay = 'MAC: %s (%s - %s)'.format(mac,ip, name);
hostOptions['mac:' + mac] = macDisplay;
var ipDisplay = 'IP: %s (%s - %s)'.format(ip, mac, name);
hostOptions['ip:' + ip] = ipDisplay;
});
}
});
var sortedKeys = Object.keys(hostOptions).sort(function(a, b) {
return hostOptions[a].localeCompare(hostOptions[b]);
});
sortedKeys.forEach(function(key) {
if (key.startsWith('ip:')) {
o.value(key.substring(3), hostOptions[key]);
}
});
sortedKeys.forEach(function(key) {
if (key.startsWith('mac:')) {
o.value(key.substring(4), hostOptions[key]);
}
});
}
// 时间控制方式选择
o = s.option(cbiRichListValue, 'time_mode', _('Time Control Mode'));
o.value('period', _('Time Period Control (allow in period)'));
o.value('duration', _('Allow Duration Control (allow limited time)'));
o.value('combined', _('Combined Control (allow in period + limit duration)'));
o.default = 'period';
o.rmempty = false;
o.onchange = function(ev, mode) {
var row = this.map.findElement('id', this.cbid(this.section_id));
if (row) {
// 显示/隐藏相关字段
var startTime = row.querySelector('[data-field="timestart"]');
var endTime = row.querySelector('[data-field="timeend"]');
var duration = row.querySelector('[data-field="duration"]');
var useDuration = row.querySelector('[data-field="use_duration"]');
var resetCycle = row.querySelector('[data-field="reset_cycle"]');
if (startTime) startTime.parentElement.style.display =
(mode === 'period' || mode === 'combined') ? '' : 'none';
if (endTime) endTime.parentElement.style.display =
(mode === 'period' || mode === 'combined') ? '' : 'none';
if (duration) duration.parentElement.style.display =
(mode === 'duration' || mode === 'combined') ? '' : 'none';
if (useDuration) useDuration.parentElement.style.display =
(mode === 'combined') ? '' : 'none';
if (resetCycle) resetCycle.parentElement.style.display =
(mode === 'duration' || mode === 'combined') ? '' : 'none';
}
});
};
o = s.option(form.Value, 'timestart', _('Start Control Time'));
// 时间段控制字段
o = s.option(form.Value, 'timestart', _('Allow Start Time'));
o.placeholder = '00:00';
o.default = '00:00';
o.rmempty = false;
o.depends({ 'time_mode': 'period', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o = s.option(form.Value, 'timeend', _('Stop Control Time'));
o = s.option(form.Value, 'timeend', _('Allow End Time'));
o.placeholder = '00:00';
o.default = '00:00';
o.rmempty = false;
o.depends({ 'time_mode': 'period', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o = s.option(form.ListValue, 'week', _('Week Day (1~7)'));
// 持续时间控制字段
o = s.option(form.Value, 'duration', _('Allowed Duration (minutes)'));
o.placeholder = '60';
o.default = '60';
o.datatype = 'min(1)';
o.depends({ 'time_mode': 'duration', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o.description = _('设备上线后允许上网的分钟数,超过后将被禁止上网');
// 重置周期
o = s.option(cbiRichListValue, 'reset_cycle', _('Reset Cycle'));
o.value('daily', _('Daily Reset'));
o.value('weekly', _('Weekly Reset'));
o.value('monthly', _('Monthly Reset'));
o.value('never', _('Never Reset (until manual reset)'));
o.default = 'daily';
o.depends({ 'time_mode': 'duration', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o.description = _('时长重置周期');
// 组合控制:是否在时间段内启用时长限制
o = s.option(form.Flag, 'use_duration', _('Enable Duration Limit in Period'));
o.default = '0';
o.depends({ 'time_mode': 'combined', '!contains': true });
o.description = _('在允许的时间段内限制上网时长');
o = s.option(form.Value, 'week', _('Week Day (1~7)'));
o.value('0', _('Everyday'));
o.value('1', _('Monday'));
o.value('2', _('Tuesday'));
@@ -258,6 +328,8 @@ return view.extend({
o.value('6,7', _('Rest Day'));
o.default = '0';
o.rmempty = false;
o.description = _('允许上网的星期');
return m.render();
}
});
@@ -1,4 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
/*
* Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
*/
'use strict';
'require dom';
'require fs';
@@ -10,7 +14,6 @@
return view.extend({
render: function () {
var css = `
/* 日志框文本区域 */
#log_textarea pre {
padding: 10px; /* 内边距 */
border-bottom: 1px solid #ddd; /* 边框颜色 */
@@ -20,15 +23,34 @@ return view.extend({
word-wrap: break-word;
overflow-y: auto;
}
/* 5s 自动刷新文字 */
.cbi-section small {
margin-left: 1rem;
font-size: small;
color: #666; /* 深灰色文字 */
}
.log-container {
display: flex;
flex-direction: column;
max-height: 1200px;
overflow-y: auto;
border-radius: 3px;
margin-top: 10px;
padding: 5px;
}
.log-line {
padding: 3px 0;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
}
.log-line:last-child {
border-bottom: none;
}
.log-timestamp {
margin-right: 10px;
}
`;
var log_textarea = E('div', { 'id': 'log_textarea' },
var log_container = E('div', { 'class': 'log-container', 'id': 'log_container' },
E('img', {
'src': L.resource(['icons/loading.gif']),
'alt': _('Loading...'),
@@ -38,6 +60,76 @@ return view.extend({
var log_path = '/var/log/timecontrol.log';
var lastLogContent = '';
var lastScrollTop = 0;
var isScrolledToTop = true;
// 解析日志行的时间戳,用于排序
function parseLogTimestamp(logLine) {
// 假设日志格式为: [2024-01-01 12:00:00] INFO: some message
var timestampMatch = logLine.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
if (timestampMatch) {
return new Date(timestampMatch[1]).getTime();
}
return Date.now();
}
function reverseLogLines(logContent) {
if (!logContent || logContent.trim() === '') {
return logContent;
}
var lines = logContent.split('\n');
lines = lines.filter(function(line) {
return line.trim() !== '';
});
lines.sort(function(a, b) {
var timeA = parseLogTimestamp(a);
var timeB = parseLogTimestamp(b);
return timeB - timeA; // 降序排列
});
return lines.join('\n');
}
function formatLogLines(logContent, isNewContent) {
if (!logContent || logContent.trim() === '') {
return E('div', { 'class': 'log-line' }, _('Log is clean.'));
}
var lines = logContent.split('\n');
var formattedLines = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line === '') continue;
var timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
var timestampSpan = null;
var messageSpan = null;
var lineClass = 'log-line';
if (timestampMatch) {
timestampSpan = E('span', {
'class': 'log-timestamp',
'title': timestampMatch[1]
}, timestampMatch[0] + ' ');
messageSpan = E('span', {}, line.substring(timestampMatch[0].length + 1));
} else {
messageSpan = E('span', {}, line);
}
var lineDiv = E('div', { 'class': lineClass }, [
timestampSpan,
messageSpan
].filter(function(el) { return el !== null; }));
formattedLines.push(lineDiv);
}
return E('div', {}, formattedLines);
}
var clear_log_button = E('div', {}, [
E('button', {
@@ -53,9 +145,10 @@ return view.extend({
button.disabled = false;
button.textContent = _('Clear Logs');
// 立即刷新日志显示框
var log = E('pre', { 'wrap': 'pre' }, [_('Log is clean.')]);
dom.content(log_textarea, log);
lastLogContent = '';
var logContent = _('Log is clean.');
lastLogContent = logContent;
dom.content(log_container, formatLogLines(logContent, false));
isScrolledToTop = true; // 清空日志后,保持在顶部
})
.catch(function () {
button.textContent = _('Failed to clear log.');
@@ -65,34 +158,62 @@ return view.extend({
}
}, _('Clear Logs'))
]);
log_container.addEventListener('scroll', function() {
lastScrollTop = this.scrollTop;
isScrolledToTop = this.scrollTop <= 1;
});
poll.add(L.bind(function () {
return fs.read_direct(log_path, 'text')
.then(function (res) {
var newContent = res.trim() || _('Log is clean.');
if (newContent !== lastLogContent) {
var log = E('pre', { 'wrap': 'pre' }, [newContent]);
dom.content(log_textarea, log);
log.scrollTop = log.scrollHeight;
lastLogContent = newContent;
var logContent = res.trim();
if (logContent === '') {
logContent = _('Log is clean.');
}
// 检查内容是否有变化
if (logContent !== lastLogContent) {
var isNewContent = lastLogContent !== '' && lastLogContent !== _('Log is clean.');
var reversedLog = reverseLogLines(logContent);
// 格式化为HTML
var formattedLog = formatLogLines(reversedLog, isNewContent);
var prevScrollHeight = log_container.scrollHeight;
var prevScrollTop = log_container.scrollTop;
dom.content(log_container, formattedLog);
lastLogContent = logContent;
if (isScrolledToTop || isNewContent) {
log_container.scrollTop = 0;
} else {
var newScrollHeight = log_container.scrollHeight;
var heightDiff = newScrollHeight - prevScrollHeight;
log_container.scrollTop = prevScrollTop + heightDiff;
}
}
}).catch(function (err) {
var log;
var logContent;
if (err.toString().includes('NotFoundError')) {
log = E('pre', { 'wrap': 'pre' }, [_('Log file does not exist.')]);
logContent = _('Log file does not exist.');
} else {
log = E('pre', { 'wrap': 'pre' }, [_('Unknown error: %s').format(err)]);
logContent = _('Unknown error: %s').format(err);
}
if (logContent !== lastLogContent) {
dom.content(log_container, formatLogLines(logContent, false));
lastLogContent = logContent;
}
dom.content(log_textarea, log);
});
}));
// 启动轮询
poll.start();
return E('div', { 'class': 'cbi-map' }, [
E('style', [css]),
E('div', { 'class': 'cbi-section' }, [
clear_log_button,
log_textarea,
log_container,
E('small', {}, _('Refresh every 5 seconds.').format(L.env.pollinterval)),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
@@ -100,7 +221,7 @@ return view.extend({
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy/luci-app-timecontrol',
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')