diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css deleted file mode 100644 index a99733e18..000000000 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.css +++ /dev/null @@ -1,515 +0,0 @@ -#chartjs-tooltip { - opacity: 0; - position: absolute; - background: rgba(0, 0, 0, .7); - color: white; - padding: 3px; - border-radius: 3px; - transition: all .1s ease; - pointer-events: none; - transform: translate(-50%, 0); - z-index: 20000; -} - -#chartjs-tooltip.above { - transform: translate(-50%, -100%); -} - -#chartjs-tooltip.above:before { - border: solid; - border-color: #111 transparent; - border-color: rgba(0, 0, 0, .8) transparent; - border-width: 8px 8px 0 8px; - bottom: 1em; - content: ""; - display: block; - left: 50%; - top: 100%; - position: absolute; - z-index: 99; - transform: translate(-50%, 0); -} - -.pie label { - font-weight: bold; - font-size: 14px; - display: block; - margin-bottom: 10px; - text-align: center; -} - -.kpi ul { - list-style: none; -} - -.kpi li { - margin: 10px; - display: none; -} - -.kpi big { - font-weight: bold; -} - -.head { - text-align: center; - position: relative; - display: flex; - flex-wrap: wrap; - white-space: normal; -} - -.head .pie { - /* min-width: 200px; */ - padding: 5px; - flex: 1 1 30%; -} - -.cbi-tooltip .head .pie { - min-width: 100px; -} - -.head .kpi { - padding: 5px; - font-size: smaller; - text-align: left; - align-self: center; - flex: 1 0 33%; - min-width: 150px; - display: flex; - justify-content: center; -} - -.head .kpi ul { - margin: 0; -} - -.td.double > span { - display: block; -} - -.cbi-tooltip { - box-shadow: 0 0 5px #000; -} - -@media screen and (max-width: 992px) { - .td.hide-xs { - display: none; - } - - .td.double:not(.hide-xs) > span { - white-space: nowrap; - text-align: left; - } - - .td.double:not(.hide-xs) > span:first-child::before { - content: "IPv4: "; - font-weight: bold; - } - - .td.double:not(.hide-xs) > span:last-child::before { - content: "IPv6: "; - font-weight: bold; - } -} - -.form-group { - display: flex; - align-items: center; /* 垂直居中 */ - margin-bottom: 15px; /* 项间距 */ - gap: 10px; /* Label 和 Input 间距 */ -} - -/* 标签固定最小宽度 + 右对齐 */ -.form-label { - flex: 0 0 120px; /* 不伸缩、不收缩、基础宽度120px */ - text-align: right; - font-weight: bold; - color: #333; -} - -.th-sort-asc::after { - content: " ▲"; -} - -.th-sort-desc::after { - content: " ▼"; -} - -.table .th { - cursor: pointer; -} - -.table .th:hover { - background-color: #f0f0f0; -} - -/* L7 View Specific Styles */ -.l7-view-container #l7-error-message { color: red; background-color: #ffefef; border: 1px solid red; padding: 10px; margin-bottom: 10px; display: none; } -.l7-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; } -.l7-view-container .line-chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; } -.l7-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } -.l7-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; } -.l7-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; } -.l7-view-container .kpi-card-label { font-size: 0.9em; color: #666; } -.l7-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } -.l7-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; } -.l7-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; } -.l7-view-container .l7-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 20px; - padding: 15px 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); - color: white; -} -.l7-view-container .l7-controls-left, .l7-view-container .l7-controls-right { - display: flex; - align-items: center; - gap: 15px; -} - -/* Display View Specific Styles */ -.display-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; } -.display-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; } -.display-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; } -.display-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; } -.display-view-container .kpi-card-label { font-size: 0.9em; color: #666; } -.display-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } -.display-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; } -.display-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; } -.display-view-container .display-controls { - display: flex; - justify-content: space-between; - align-items: center; - padding: 15px 20px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-radius: 8px; - box-shadow: 0 4px 6px rgba(0,0,0,0.1); - margin-top: 20px; -} - -/* Control Groups */ -.control-group { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - background-color: rgba(255, 255, 255, 0.15); - border-radius: 6px; - backdrop-filter: blur(10px); -} - -.control-icon { - font-size: 1.2em; - display: inline-block; -} - -.control-label { - font-weight: 500; - color: white; - margin: 0; - white-space: nowrap; -} - -.control-input { - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.3); - background-color: rgba(255, 255, 255, 0.9); - padding: 6px 12px; - transition: all 0.3s ease; -} - -.control-input:focus { - background-color: white; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); - outline: none; -} - -.control-buttons { - display: flex; - align-items: center; - gap: 10px; -} - -.status-group { - background-color: rgba(255, 255, 255, 0.2); -} - -.last-updated-text { - color: rgba(255, 255, 255, 0.95); - font-size: 0.9em; - font-weight: 500; -} - -/* Enhanced Select Styling */ -.cbi-input-select { - padding: 6px 30px 6px 12px; - border-radius: 4px; - border: 1px solid rgba(255, 255, 255, 0.3); - background-color: rgba(255, 255, 255, 0.9); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 12px; - appearance: none; - cursor: pointer; - transition: all 0.3s ease; - font-weight: 500; -} - -.cbi-input-select:hover { - background-color: white; - border-color: #667eea; -} - -.cbi-input-select:focus { - background-color: white; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); - outline: none; -} - -/* Button Enhancements for Controls */ -.l7-controls .cbi-button, -.display-controls .cbi-button { - padding: 8px 16px; - border-radius: 6px; - font-weight: 500; - transition: all 0.3s ease; - border: 2px solid transparent; -} - -.cbi-button-action { - background: rgba(255, 193, 7, 0.9); - color: #333; - border-color: rgba(255, 193, 7, 0.3); -} - -.cbi-button-action:hover { - background: #ffc107; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(255, 193, 7, 0.4); -} - -.cbi-button-positive { - background: rgba(40, 167, 69, 0.9); - color: white; - border-color: rgba(40, 167, 69, 0.3); -} - -.cbi-button-positive:hover { - background: #28a745; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4); -} - -/* Responsive Controls */ -@media screen and (max-width: 768px) { - .l7-controls, - .display-controls { - flex-direction: column; - gap: 15px; - } - - .l7-controls-left, - .l7-controls-right, - .control-buttons { - width: 100%; - justify-content: center; - } - - .control-group { - justify-content: center; - } -} - -/* Table Cell Icon Styles */ -.activity-indicator { - font-size: 12px; - display: inline-block; - margin-right: 2px; -} - -.icon, .btn-icon, .th-icon { - display: inline-block; - font-style: normal; - margin-right: 4px; -} - -.th-icon { - opacity: 0.7; -} - -/* Cell Specific Styles */ -.sid-cell, .id-cell, .host-cell, .hostname-cell, .protocol-cell { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.speed-cell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - font-weight: 500; - min-width: 100px; -} - -.speed-cell.download { - color: #28a745; -} - -.speed-cell.upload { - color: #007bff; -} - -.volume-cell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - min-width: 90px; -} - -.volume-cell.download { - color: #17a2b8; -} - -.volume-cell.upload { - color: #6610f2; -} - -.packet-cell { - display: inline-flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - color: #6c757d; - min-width: 80px; -} - -/* Data value with monospace font for better alignment */ -.data-value { - font-family: 'Courier New', Consolas, monospace; - font-size: 0.95em; - letter-spacing: 0.5px; -} - -/* Table header enhancements */ -.table .th { - font-weight: 600; - padding: 10px 8px; - white-space: nowrap; -} - -.table .td { - padding: 8px; - vertical-align: middle; -} - -.protocol-icon.l7::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: #5470c6; - margin-right: 4px; -} - -.protocol-icon.domain::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: #91cc75; - margin-right: 4px; -} - -/* Button Enhancements */ -.cbi-button { - display: inline-flex; - align-items: center; - gap: 4px; - transition: all 0.2s ease; -} - -.cbi-button:hover { - transform: translateY(-1px); - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.cbi-button-edit { - background-color: #ffc107; - border-color: #ffc107; -} - -.cbi-button-edit:hover { - background-color: #e0a800; - border-color: #e0a800; -} - -.cbi-button-remove { - background-color: #dc3545; - border-color: #dc3545; - color: white; -} - -.cbi-button-remove:hover { - background-color: #c82333; - border-color: #bd2130; -} - -.cbi-button-add { - background-color: #28a745; - border-color: #28a745; - color: white; -} - -.cbi-button-add:hover:not(:disabled) { - background-color: #218838; - border-color: #1e7e34; -} - -.cbi-button-add:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -/* Table Row Hover Effects */ -.table .tr:not(.table-titles):not(.placeholder):hover { - background-color: #f8f9fa; - transition: background-color 0.2s ease; -} - -/* Active/Inactive Status Styles */ -.activity-indicator[title*="Active"] { - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* Responsive Icon Handling */ -@media screen and (max-width: 768px) { - .icon, .btn-icon { - margin-right: 2px; - } - - .btn-icon + span { - display: none; - } - - .th-icon { - margin-right: 0; - } -} diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js index 426085294..1ac0fa375 100644 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js +++ b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx.js @@ -30,219 +30,6 @@ var callGetWanMac = rpc.declare({ expect: { result: {} } }); -// Generate Device ID: AW + 7 random digits + 12-char MAC (total 21 chars) -function generateDeviceId(macAddress) { - var macPart = macAddress.replace(/-/g, ''); // Remove hyphens from MAC - var randomPart = ''; - for (var i = 0; i < 7; i++) { - randomPart += Math.floor(Math.random() * 10); - } - return 'AW' + randomPart + macPart; -} - -// Generate Location ID: 14 digits (AAAAAABBBCCCCC) -// AAAAAA: Administrative division code (GB/T 2260) - using Beijing as default (110101) -// BBB: Service type (100 for commercial internet service locations, positions 7-9) -// CCCCC: 5-digit random sequence number (positions 10-14) -function generateLocationId() { - var adminCode = '110101'; // Default to Beijing Dongcheng District (positions 1-6) - var serviceType = '100'; // Commercial internet service location (positions 7-9) - var sequenceNum = ''; - - // Generate 5-digit random sequence number (positions 10-14) - for (var i = 0; i < 5; i++) { - sequenceNum += Math.floor(Math.random() * 10); - } - - return adminCode + serviceType + sequenceNum; // 6 + 3 + 5 = 14 digits -} - -// Auto-fill helper functions -function showAutoFillModal() { - ui.showModal(_('Auto Fill'), [ - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'cbi-section-descr' }, _('Getting device information and location data, please wait...')), - E('div', { 'id': 'auto-fill-progress' }, [ - E('ul', { 'style': 'margin: 10px 0;' }, [ - E('li', { 'id': 'mac-status' }, _('Getting WAN MAC address...')), - E('li', { 'id': 'device-id-status' }, _('Generating Device ID...')), - E('li', { 'id': 'location-id-status' }, _('Generating Location ID...')), - E('li', { 'id': 'location-status' }, _('Getting location coordinates...')) - ]) - ]) - ]) - ]); -} - -function updateMacStatus(macAddr, isSuccess, errorMsg, isFallback) { - var statusElement = document.getElementById('mac-status'); - if (isSuccess) { - var prefix = isFallback ? _('✓ WAN MAC address obtained (fallback): ') : _('✓ WAN MAC address obtained: '); - var suffix = isFallback ? '' : _(' (interface: ') + arguments[4] + _(')'); - statusElement.innerHTML = prefix + macAddr + suffix; - } else { - statusElement.innerHTML = _('✗ Failed to get WAN MAC address: ') + (errorMsg || _('Unknown error')); - } -} - -function generateAndUpdateIds(macAddr) { - var deviceId = generateDeviceId(macAddr); - var locationId = generateLocationId(); - - document.getElementById('device-id-status').innerHTML = _('✓ Device ID generated: ') + deviceId; - document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + locationId; - - return { deviceId: deviceId, locationId: locationId }; -} - -function getMacAddressPromise(results) { - return callGetWanMac().then(function (response) { - if (response && response.status === 'success') { - var macAddr = response.mac.toUpperCase().replace(/:/g, '-'); - results.macAddress = macAddr; - updateMacStatus(macAddr, true, null, false, response.interface); - - var ids = generateAndUpdateIds(macAddr); - results.deviceId = ids.deviceId; - results.locationId = ids.locationId; - } else { - return getMacAddressFallback(results); - } - }).catch(function (error) { - return getMacAddressFallback(results); - }); -} - -function getMacAddressFallback(results) { - document.getElementById('mac-status').innerHTML = _('RPC failed, trying fallback...'); - - return fs.exec('/bin/sh', ['-c', 'ip route | grep default | awk \'{print $5}\' | head -n1 | xargs -I {} cat /sys/class/net/{}/address 2>/dev/null || echo "failed"']).then(function (response) { - if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') { - var macAddr = response.stdout.trim().toUpperCase().replace(/:/g, '-'); - results.macAddress = macAddr; - updateMacStatus(macAddr, true, null, true); - - var ids = generateAndUpdateIds(macAddr); - results.deviceId = ids.deviceId; - results.locationId = ids.locationId; - } else { - updateMacStatus(null, false, _('Unknown error')); - document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID without MAC address'); - } - }).catch(function (error) { - updateMacStatus(null, false, error.message); - document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID due to MAC error'); - }); -} - -function formatCoordinate(coord) { - var num = parseFloat(coord).toFixed(6); - if (num >= 0) { - return num.padStart(10, '0'); - } else { - return '-' + Math.abs(num).toFixed(6).padStart(9, '0'); - } -} - -function getLocationPromise(results) { - return callGetLocation().then(function (response) { - if (response && response.status === 'success') { - var lat = formatCoordinate(response.lat); - var lon = formatCoordinate(response.lon); - - results.longitude = lon; - results.latitude = lat; - document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + lat + ', ' + lon; - } else { - var errorMsg = (response && response.message) ? response.message : _('Unknown error'); - document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg; - } - }).catch(function (error) { - return getLocationFallback(results); - }); -} - -function getLocationFallback(results) { - document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...'); - - return fs.exec('/bin/sh', ['-c', 'curl -s --connect-timeout 10 --max-time 30 "https://ipapi.co/json" 2>/dev/null || curl -s --connect-timeout 10 --max-time 30 "http://ipinfo.io/json" 2>/dev/null || echo "failed"']).then(function (response) { - if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') { - try { - var data = JSON.parse(response.stdout); - var lat, lon; - - if (data.latitude && data.longitude) { - lat = formatCoordinate(data.latitude); - lon = formatCoordinate(data.longitude); - } else if (data.loc) { - var coords = data.loc.split(','); - lat = formatCoordinate(coords[0]); - lon = formatCoordinate(coords[1]); - } - - if (lat && lon) { - results.longitude = lon; - results.latitude = lat; - document.getElementById('location-status').innerHTML = _('✓ Location obtained (fallback): ') + lat + ', ' + lon; - } else { - document.getElementById('location-status').innerHTML = _('✗ No valid coordinates found'); - } - } catch (e) { - document.getElementById('location-status').innerHTML = _('✗ Failed to parse location data'); - } - } else { - document.getElementById('location-status').innerHTML = _('✗ Failed to get location data'); - } - }).catch(function (fallbackError) { - document.getElementById('location-status').innerHTML = _('✗ Error getting location: ') + fallbackError.message; - }); -} - -function fillFormFields(results) { - var successCount = 0; - var totalFields = 0; - var messages = []; - var fieldMappings = [ - { result: 'macAddress', selector: 'ap_mac_address', label: _('MAC Address: ') }, - { result: 'deviceId', selector: 'ap_device_id', label: _('Device ID: ') }, - { result: 'locationId', selector: 'location_id', label: _('Location ID: ') }, - { result: 'longitude', selector: 'ap_longitude', label: _('Longitude: ') }, - { result: 'latitude', selector: 'ap_latitude', label: _('Latitude: ') } - ]; - - fieldMappings.forEach(function (mapping) { - if (results[mapping.result]) { - var field = document.querySelector('input[data-name="' + mapping.selector + '"]') || - document.querySelector('input[name*="' + mapping.selector + '"]') || - document.querySelector('input[id*="' + mapping.selector + '"]') || - document.querySelector('#cbid\\.wifidogx\\.default\\.' + mapping.selector); - - if (field) { - field.value = results[mapping.result]; - field.dispatchEvent(new Event('input', { bubbles: true })); - field.dispatchEvent(new Event('change', { bubbles: true })); - successCount++; - messages.push(mapping.label + results[mapping.result]); - } - totalFields++; - } - }); - - return { successCount: successCount, totalFields: totalFields, messages: messages }; -} - -function showAutoFillResult(stats) { - ui.hideModal(); - - if (stats.successCount === stats.totalFields && stats.totalFields > 0) { - ui.addNotification(null, E('p', _('Auto fill completed successfully! All fields have been filled.') + '
' + stats.messages.join('
')), 'info'); - } else if (stats.successCount > 0) { - ui.addNotification(null, E('p', _('Auto fill partially completed.') + ' ' + stats.successCount + '/' + stats.totalFields + ' ' + _('fields filled successfully.') + '
' + stats.messages.join('
')), 'warning'); - } else { - ui.addNotification(null, E('p', _('Auto fill failed. No fields could be filled. Please check your network connection and try again.')), 'error'); - } -} - function getServiceStatus() { return L.resolveDefault(callServiceList('wifidogx'), {}).then(function (res) { var isRunning = false; @@ -253,6 +40,25 @@ function getServiceStatus() { }); } +function generateDeviceId(macAddress) { + var macPart = macAddress.replace(/-/g, ''); + var randomPart = ''; + for (var i = 0; i < 7; i++) { + randomPart += Math.floor(Math.random() * 10); + } + return 'AW' + randomPart + macPart; +} + +function generateLocationId() { + var adminCode = '110101'; + var serviceType = '100'; + var sequenceNum = ''; + for (var i = 0; i < 5; i++) { + sequenceNum += Math.floor(Math.random() * 10); + } + return adminCode + serviceType + sequenceNum; +} + function renderStatus(isRunning) { var renderHTML = ""; var spanTemp = '%s %s'; @@ -284,7 +90,6 @@ return view.extend({ s.tab('gateway', _('Gateway Settings')); s.tab('advanced', _('Advanced Settings')); s.tab('rule', _('Rule Settings')); - s.tab('qos', _('QoS Settings')); s.tab('location', _('Authentication Location Settings')); // basic settings @@ -368,7 +173,7 @@ return view.extend({ o = s.taboption('basic', form.Flag, 'disable_portal_auth', _('Disable Portal Authentication'), _('When enabled, users can access the internet without portal authentication. Firewall redirect rules will not be created. Use this mode for pure traffic statistics without captive portal.')); o.rmempty = false; - o.default = '1'; + o.default = '0'; o = s.taboption('basic', form.ListValue, 'log_level', _('Log Level'), _('The log level of the apfree-wifidog.')); @@ -380,7 +185,7 @@ return view.extend({ o.value(2, _('Critical')); o.value(1, _('Alert')); o.value(0, _('Emergency')); - o.defaulValue = 0; + o.default = 0; o.optional = false; // gateway settings @@ -392,7 +197,7 @@ return view.extend({ o = ss.option(form.Flag, 'gateway_auth_enabled', _('Auth Enabled'), _('Enable the authentication of the gateway.')); o.rmempty = false; - o.defaulValue = true; + o.default = true; o = ss.option(widgets.DeviceSelect, 'gateway_name', _('Gateway Name')); o.rmempty = false; @@ -425,14 +230,14 @@ return view.extend({ o.datatype = 'uinteger'; o.rmempty = false; o.optional = false; - o.defaulValue = 60; + o.default = 60; o = s.taboption('advanced', form.Value, 'client_timeout', _('Client Timeout'), _('The timeout of the client.')); o.datatype = 'uinteger'; o.rmempty = false; o.optional = false; - o.defaulValue = 5; + o.default = 5; o = s.taboption('advanced', form.Flag, 'wired_passed', _('Wired Passed'), _('Wired users do not need to authenticate to access the internet.')); @@ -441,23 +246,30 @@ return view.extend({ o = s.taboption('advanced', form.Flag, 'apple_cna', _('Apple CNA'), _('Enable Apple Captive Network Assistant.')); o.rmempty = false; - o.defaulValue = false; + o.default = false; o = s.taboption('advanced', form.Flag, 'js_filter', _('JS Filter'), _('Enable JS redirect.')); o.rmempty = false; - o.defaulValue = true; + o.default = true; o = s.taboption('advanced', form.Flag, 'enable_anti_nat', _('Enable Anti NAT'), _('Enable Anti NAT devices.')); o.rmempty = false; - o.defaulValue = false; + o.default = false; + + o = s.taboption('advanced', form.Value, 'privileged_ops_secret', _('Privileged Ops Secret'), + _('Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices.')); + o.password = true; + o.rmempty = false; + o.optional = false; + o.default = 'chawrt@2026'; o = s.taboption('advanced', form.Value, 'ttl_value', _('TTL Value'), _('The TTL value of the gateway support.')); o.datatype = 'string'; o.rmempty = false; - o.defaulValue = '64,128'; + o.default = '64,128'; o.depends('enable_anti_nat', '1'); o = s.taboption('advanced', form.Value, 'anti_nat_permit_macs', _('Anti NAT Permit MAC'), @@ -466,17 +278,87 @@ return view.extend({ o.rmempty = true; o.depends('enable_anti_nat', '1'); - o = s.taboption('advanced', form.Flag, 'enable_event_log', _('Enable Internet Access Log'), - _('Enable logging of internet access events and user activities.')); - o.rmempty = false; - o.defaulValue = false; - // Auth Server Settings o = s.taboption('auth', form.SectionValue, '_auth', form.GridSection, 'auth'); ss = o.subsection; ss.addremove = true; ss.nodescriptions = true; + ss.handleAdd = function (ev, name) { + var self = this; + if (name) { + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + } + return this.super('handleAdd', [ev, name]); + }; + + ss.handleRename = function (section_id, ev) { + var self = this; + var oldName = section_id; + var nameEl = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'auth-rename-input', + 'value': oldName, + 'style': 'width: 100%;' + }); + ui.showModal(_('Rename Authentication Server'), [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this authentication server profile.')), + E('div', { 'class': 'cbi-section-table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, _('Name')), + E('div', { 'class': 'td' }, [nameEl]) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')), + E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () { + var newName = nameEl.value.trim(); + if (!newName) { + ui.addNotification(null, E('p', _('Please enter a name.')), 'error'); + return; + } + if (newName === oldName) { + ui.hideModal(); + return; + } + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === newName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === newName && authSections[i]['.name'] !== oldName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + ui.hideModal(); + uci.rename('wifidogx', section_id, newName); + self.map.save(null, true); + }}, _('Rename')) + ]) + ]); + }; + o = ss.option(form.Value, 'auth_server_hostname', _('Auth Server Hostname'), _('The domain or IP address of the authentication server.')); o.rmempty = false; @@ -499,29 +381,6 @@ return view.extend({ o.optional = false; o.default = '/wifidog/'; - - // QoS settings - o = s.taboption('qos', form.Flag, 'enable_qos', _('Enable Global QoS'), - _('Enable Global QoS.')); - o.rmempty = false; - o.defaulValue = false; - - o = s.taboption('qos', form.Value, 'qos_up', _('Global QoS Up'), - _('The global QoS up value(Mbps).')); - o.datatype = 'uinteger'; - o.rmempty = true; - o.optional = true; - o.defaulValue = 0; - o.depends('enable_qos', '1'); - - o = s.taboption('qos', form.Value, 'qos_down', _('Global QoS Down'), - _('The global QoS down value(Mbps).')); - o.datatype = 'uinteger'; - o.rmempty = true; - o.optional = true; - o.defaulValue = 0; - o.depends('enable_qos', '1'); - // Authentication Location Settings // Add auto-fill button o = s.taboption('location', form.Button, '_auto_fill', _('Auto Fill All Fields'), @@ -544,11 +403,8 @@ return view.extend({ getLocationPromise(results) ]; - // Wait for all promises to complete and fill form fields Promise.all(promises).then(function () { - setTimeout(function () { - fillFormFields(results); - }, 2000); + fillFormFields(results); }); }; @@ -583,10 +439,11 @@ return view.extend({ results.locationId = generateLocationId(); document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + results.locationId; } else { + document.getElementById('mac-status').innerHTML = _('RPC returned error, trying fallback...'); return getMacAddressFallback(results); } }).catch(function (error) { - document.getElementById('mac-status').innerHTML = _('✗ RPC call failed, trying fallback...'); + document.getElementById('mac-status').innerHTML = _('RPC call failed, trying fallback...'); return getMacAddressFallback(results); }); } @@ -625,8 +482,8 @@ return view.extend({ formatAndSetCoordinates(results, lat, lon); document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + results.latitude + ', ' + results.longitude; } else { - var errorMsg = (response && response.message) ? response.message : 'Unknown error'; - document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg; + document.getElementById('location-status').innerHTML = _('RPC returned error, trying fallback...'); + return getLocationFallback(results); } }).catch(function (error) { document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...'); @@ -741,8 +598,6 @@ return view.extend({ o.rmempty = false; o.datatype = 'string'; o.optional = false; - o.depends('auth_server_mode', 'cloud'); - o.depends('auth_server_mode', 'bypass'); o.placeholder = '110101100123456'; o.validate = function (section_id, value) { if (!value || value === '') @@ -766,7 +621,8 @@ return view.extend({ // Validate service type code (7-9 digits) var serviceType = value.substring(6, 9); - if (serviceType !== '100' && serviceType !== '2' + value.charAt(7) + value.charAt(8) && serviceType !== '3' + value.charAt(7) + value.charAt(8)) { + var firstDigit = serviceType.charAt(0); + if (serviceType !== '100' && firstDigit !== '2' && firstDigit !== '3') { return _('Service type code (7-9 digits) must be "100" for commercial internet service locations, "2XX" for non-commercial locations, or "3XX" for WiFi wireless collection terminals'); } @@ -936,6 +792,81 @@ return view.extend({ ss.addremove = true; ss.nodescriptions = true; + ss.handleAdd = function (ev, name) { + var self = this; + if (name) { + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === name) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error'); + return Promise.resolve(); + } + } + } + return this.super('handleAdd', [ev, name]); + }; + + ss.handleRename = function (section_id, ev) { + var self = this; + var oldName = section_id; + var nameEl = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'id': 'longconn-rename-input', + 'value': oldName, + 'style': 'width: 100%;' + }); + ui.showModal(_('Rename Long Connection Profile'), [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this long connection profile.')), + E('div', { 'class': 'cbi-section-table' }, [ + E('div', { 'class': 'tr' }, [ + E('div', { 'class': 'td' }, _('Name')), + E('div', { 'class': 'td' }, [nameEl]) + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')), + E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () { + var newName = nameEl.value.trim(); + if (!newName) { + ui.addNotification(null, E('p', _('Please enter a name.')), 'error'); + return; + } + if (newName === oldName) { + ui.hideModal(); + return; + } + var authSections = uci.sections('wifidogx', 'auth'); + for (var i = 0; i < authSections.length; i++) { + if (authSections[i]['.name'] === newName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + var longconnSections = uci.sections('wifidogx', 'longconn'); + for (var i = 0; i < longconnSections.length; i++) { + if (longconnSections[i]['.name'] === newName && longconnSections[i]['.name'] !== oldName) { + ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error'); + return; + } + } + ui.hideModal(); + uci.rename('wifidogx', section_id, newName); + self.map.save(null, true); + }}, _('Rename')) + ]) + ]); + }; + o = ss.option(form.ListValue, 'long_conn_mode', _('Connection Mode'), _('The type of persistent connection to the remote management server.')); o.value('ws', _('WebSocket (ws://)')); @@ -1011,7 +942,7 @@ return view.extend({ // rule settings o = s.taboption('rule', form.DynamicList, 'trusted_wildcard_domains', _('Trusted Wildcard Domains'), - _('The trusted wildcard domains of the gateway')); + _('Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf.')); o.rmempty = true; o.optional = true; o.datatype = 'wildcard'; @@ -1025,7 +956,7 @@ return view.extend({ o.placeholder = 'www.example.com'; o = s.taboption('rule', form.DynamicList, 'trusted_macs', _('Trusted MACs'), - _('The trusted wildcard domains of the gateway.')); + _('The trusted MAC addresses of the gateway.')); o.rmempty = true; o.optional = true; o.datatype = 'macaddr'; diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js deleted file mode 100644 index 644dc872a..000000000 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/display.js +++ /dev/null @@ -1,650 +0,0 @@ -'use strict'; -'require view'; -'require fs'; -'require ui'; -'require poll'; -'require rpc'; -'require dom'; -'require uci'; - -// Global variables from original display.js -var chartRegistry = {}; -var hostNames = {}; // mac => hostname -var hostInfo = {}; // ip => mac -var hostNameMacSectionId = ""; -var isPaused = false; -var lastUpdated = null; - -// Line chart variables (from l7.js) -var downloadLineChart = {}, uploadLineChart = {}; -var lineCategories = { ipv4: [], ipv6: [], mac: [] }; -var downloadSeriesData = { ipv4: {}, ipv6: {}, mac: {} }; -var uploadSeriesData = { ipv4: {}, ipv6: {}, mac: {} }; - -// Color palette for chart series -var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']; - -var resizeListenerAdded = false; - -// Pre-fill with 60 empty points for a smooth start -['ipv4', 'ipv6', 'mac'].forEach(function(type) { - for (var i = 0; i < 60; i++) { - lineCategories[type].push(''); - } -}); - -// Helper to convert hex to rgba (from l7.js) -function hexToRgba(hex, opacity) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? - 'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' : - null; -}; - -return view.extend({ - // --- Core Data Logic from display.js --- - - loadHostNames: async function() { - try { - await uci.sections('hostnames', "hostname", function (params) { - hostNameMacSectionId = params['.name']; - for (var key in params) { - if (key.startsWith('.')) continue; - var macAddr = key.split('_').join(':'); - hostNames[macAddr] = params[key]; - } - }); - - const dhcpLeases = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $2, $3, $4}', '/tmp/dhcp.leases'], 'text'); - dhcpLeases.split('\n').forEach(function(line) { - if (line === '') return; - const [mac, ip, hostname] = line.split(' '); - if (!hostNames.hasOwnProperty(mac)) { - hostNames[mac] = hostname; - } - }); - - const arp = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $1, $4}', '/proc/net/arp'], 'text'); - arp.split('\n').forEach(function(line, i) { - if (i === 0 || line === '') return; - const [ip, mac] = line.split(' '); - hostInfo[ip] = mac; - }); - - } catch (e) { - console.error('Error getting host names:', e); - } - }, - - loadHostSpeedData: async function() { - var self = this; - try { - const results = await Promise.all([ - fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv4', 'json'], 'json'), - fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv6', 'json'], 'json'), - fs.exec_direct('/usr/bin/aw-bpfctl', ['mac', 'json'], 'json') - ]); - - const defaultData = {status: "success", data: []}; - const ipv4Data = results[0] || defaultData; - const ipv6Data = results[1] || defaultData; - const macData = results[2] || defaultData; - - ipv4Data.data.forEach(function(item) { - const mac = hostInfo[item.ip]; - if (mac) { - item.mac = mac; - item.hostname = hostNames[mac]; - } - }); - macData.data.forEach(function(item) { - const mac = item.mac; - if (mac) { - item.hostname = hostNames[mac]; - } - }); - - self.renderHostSpeed(ipv4Data, "ipv4"); - self.renderHostSpeed(ipv6Data, "ipv6"); - self.renderHostSpeed(macData, "mac"); - - lastUpdated = new Date(); - var timestampEl = document.getElementById('display-last-updated'); - if (timestampEl) { - timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString()); - } - - } catch (e) { - console.error('Error polling data:', e); - } - }, - - pollData: function() { - poll.add(L.bind(async function() { - if (isPaused) return; - await this.loadHostNames(); - await this.loadHostSpeedData(); - }, this), 5); - }, - - // --- UI Rendering and Interaction (New structure based on l7.js) --- - - pie: function(id, data, valueFormatter) { - var total = data.reduce(function(n, d) { return n + d.value; }, 0); - data.sort(function(a, b) { return b.value - a.value; }); - - if (total === 0) { - data = [{ value: 1, color: '#cccccc', name: _('no traffic') }]; - } - - data.forEach(function(d, i) { - if (!d.color) { - var hue = (i * 137.508) % 360; - d.color = 'hsl(' + hue + ', 75%, 55%)'; - } - }); - - var option = { - tooltip: { - trigger: 'item', - formatter: function(params) { - if (valueFormatter) { - // 将 ECharts params 对象转换为自定义格式 - return valueFormatter({ - name: params.name, - value: params.value, - percent: params.percent.toFixed(2) - }); - } - return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)'; - } - }, - series: [{ - type: 'pie', - radius: ['25%', '80%'], - avoidLabelOverlap: false, - padAngle: 10, - itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, - label: { show: false, position: 'center' }, - emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, - labelLine: { show: false }, - data: data.map(function(d) { - return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } }; - }) - }] - }; - - var dom = typeof id === 'string' ? document.getElementById(id) : id; - if (!chartRegistry[id]) { - chartRegistry[id] = echarts.init(dom); - } - chartRegistry[id].setOption(option, true); - return chartRegistry[id]; - }, - - updateStackedLineCharts: function(type, perHostDownload, perHostUpload) { - var now = new Date().toLocaleTimeString(); - lineCategories[type].push(now); - lineCategories[type].shift(); - - var processChartData = function(seriesData, perHostData) { - var allHosts = Object.keys(seriesData); - Object.keys(perHostData).forEach(function(host) { - if (allHosts.indexOf(host) === -1) { - allHosts.push(host); - } - }); - - allHosts.forEach(function(host) { - if (!seriesData[host]) { - seriesData[host] = Array(59).fill(0); - } - var rate = perHostData[host] || 0; - seriesData[host].push(rate); - seriesData[host].shift(); - }); - - return Object.keys(seriesData).map(function(host, index) { - var color = colorPalette[index % colorPalette.length]; - return { - name: host, - type: 'line', - stack: 'Total', - smooth: true, - lineStyle: { width: 1, color: color }, - showSymbol: false, - itemStyle: { color: color }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: hexToRgba(color, 0.5) }, - { offset: 1, color: hexToRgba(color, 0) } - ]) - }, - data: seriesData[host] - }; - }); - }; - - var downloadChartSeries = processChartData(downloadSeriesData[type], perHostDownload); - var uploadChartSeries = processChartData(uploadSeriesData[type], perHostUpload); - - var legendData = downloadChartSeries.map(function(s) { return s.name; }); - - if (downloadLineChart[type]) { - downloadLineChart[type].setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: downloadChartSeries, - xAxis: { data: lineCategories[type] } - }); - } - - if (uploadLineChart[type]) { - uploadLineChart[type].setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: uploadChartSeries, - xAxis: { data: lineCategories[type] } - }); - } - }, - - renderHostSpeed: function(data, type) { - if (!data || data.status !== "success" || !Array.isArray(data.data)) return; - - var rows = []; - var txRateData = [], rxRateData = []; - var txVolumeData = [], rxVolumeData = []; - var tx_rate_total = 0, rx_rate_total = 0; - var tx_bytes_total = 0, rx_bytes_total = 0; - var perHostTxRate = {}; - var perHostRxRate = {}; - - data.data.forEach(item => { - if (!item || !item.incoming || !item.outgoing) return; - - var host = item.ip || item.mac || ''; - var hostname = item.hostname || hostNames[item.mac] || ''; - var displayName = hostname || host; - - // 判断连接是否活跃 - var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0; - var activityIcon = isActive ? '🟢' : '⚪'; - - rows.push([ - E('span', { 'class': 'host-cell' }, [ - E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon), - E('span', {}, ' ' + host) - ]), - E('span', { 'class': 'hostname-cell' }, [ - E('span', { 'class': 'icon' }, hostname ? '👤' : '❓'), - E('span', {}, ' ' + (hostname || _('Unknown'))) - ]), - E('span', { 'class': 'speed-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.incoming.rate)) - ]), - E('span', { 'class': 'volume-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes)) - ]), - E('span', { 'class': 'packet-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets)) - ]), - E('span', { 'class': 'speed-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.outgoing.rate)) - ]), - E('span', { 'class': 'volume-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes)) - ]), - E('span', { 'class': 'packet-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets)) - ]), - E('div', { 'class': 'button-container' }, [ - E('button', { - 'class': 'btn cbi-button cbi-button-edit', - 'style': 'margin-right: 5px;', - 'click': ui.createHandlerFn(this, () => this.handleEditSpeed(host, item.mac, hostname, type)) - }, [ - E('span', { 'class': 'btn-icon' }, '✏️'), - E('span', {}, ' ' + _('Edit')) - ]), - E('button', { - 'class': 'btn cbi-button cbi-button-remove', - 'click': ui.createHandlerFn(this, () => this.handleDeleteHost(host, type)) - }, [ - E('span', { 'class': 'btn-icon' }, '🗑️'), - E('span', {}, ' ' + _('Delete')) - ]) - ]) - ]); - rx_rate_total += item.outgoing.rate; - tx_rate_total += item.incoming.rate; - rx_bytes_total += item.outgoing.total_bytes; - tx_bytes_total += item.incoming.total_bytes; - - rxRateData.push({ value: item.outgoing.rate, label: displayName }); - txRateData.push({ value: item.incoming.rate, label: displayName }); - rxVolumeData.push({ value: item.outgoing.total_bytes, label: displayName }); - txVolumeData.push({ value: item.incoming.total_bytes, label: displayName }); - - perHostTxRate[displayName] = (perHostTxRate[displayName] || 0) + item.incoming.rate; - perHostRxRate[displayName] = (perHostRxRate[displayName] || 0) + item.outgoing.rate; - }); - - this.updateStackedLineCharts(type, perHostTxRate, perHostRxRate); - - var table = document.getElementById(type + '-speed-data'); - cbi_update_table(table, rows, E('em', _('No data recorded yet.'))); - - this.pie(type + '-tx-rate-pie', txRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`); - this.pie(type + '-rx-rate-pie', rxRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`); - this.pie(type + '-tx-volume-pie', txVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`); - this.pie(type + '-rx-volume-pie', rxVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`); - - var hostEl = document.getElementById(type + '-host-val'); - if (hostEl) hostEl.textContent = data.data.length; - - var txRateEl = document.getElementById(type + '-tx-rate-val'); - if (txRateEl) txRateEl.textContent = '%1024.2mBps'.format(tx_rate_total); - - var rxRateEl = document.getElementById(type + '-rx-rate-val'); - if (rxRateEl) rxRateEl.textContent = '%1024.2mBps'.format(rx_rate_total); - - var txVolEl = document.getElementById(type + '-tx-volume-val'); - if (txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total); - - var rxVolEl = document.getElementById(type + '-rx-volume-val'); - if (rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total); - }, - - // --- Interaction Handlers from display.js --- - - handleDeleteHost: function(host, type) { - ui.showModal(_('Delete Host'), [ - E('p', _('Are you sure you want to delete this host?')), - E('div', { 'class': 'right' }, [ - E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')), - E('button', { 'class': 'btn cbi-button-negative', 'click': ui.createHandlerFn(this, async () => { - try { - await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'del', host], 'text'); - this.loadHostSpeedData(); - ui.hideModal(); - } catch (e) { - ui.addNotification(null, E('p', _('Error: ') + e.message)); - ui.hideModal(); - } - })}, _('Delete')) - ]) - ]); - }, - - handleEditSpeed: function(host, mac, hostname, type) { - fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'json'], 'json').then(L.bind(res => { - let rate_limit_dl = 0, rate_limit_ul = 0; - if (res && res.status === 'success' && Array.isArray(res.data)) { - const item = res.data.find(d => (d.ip === host || d.mac === host)); - if (item) { - rate_limit_dl = (item.incoming.incoming_rate_limit || 0) / 1024 / 1024; - rate_limit_ul = (item.outgoing.outgoing_rate_limit || 0) / 1024 / 1024; - } - } - this.displaySpeedLimitDialog(host, mac, hostname, type, rate_limit_dl, rate_limit_ul); - }, this)).catch(e => { - console.error('Error getting speed limit:', e); - this.displaySpeedLimitDialog(host, mac, hostname, type, 0, 0); - }); - }, - - displaySpeedLimitDialog: function(host, mac, hostname, type, dl, ul) { - const inputDom = E('input', { type: 'text', id: 'host-name', class: 'cbi-input-text', value: hostname, disabled: !mac }); - - ui.showModal(_('Edit Speed Limit'), [ - E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Host')), E('span',{}, host) ]), - E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Hostname')), inputDom ]), - E('div', { 'class': 'form-group' }, [ - E('label', { 'class': 'form-label' }, _('Download Limit')), - E('input', { type: 'number', id: 'dl-rate', class: 'cbi-input-number', min: '0', value: dl }), - E('span',{}, " Mbps") - ]), - E('div', { 'class': 'form-group' }, [ - E('label', { 'class': 'form-label' }, _('Upload Limit')), - E('input', { type: 'number', id: 'ul-rate', class: 'cbi-input-number', min: '0', value: ul }), - E('span',{}, " Mbps") - ]), - E('div', { 'class': 'cbi-page-actions right' }, [ - E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'click': ui.hideModal }, _('Cancel')), - E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, async ev => { - const dl_val = document.getElementById('dl-rate').value; - const ul_val = document.getElementById('ul-rate').value; - const newName = document.getElementById('host-name').value; - try { - if (mac && newName !== hostname) { - hostNames[mac] = newName; - await uci.set('hostnames', hostNameMacSectionId, mac.split(':').join('_'), newName); - await uci.save('hostnames'); - await uci.apply('hostnames'); - } - await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'update', host, "downrate", dl_val*1024*1024 || '0', "uprate", ul_val*1024*1024 || '0']); - this.loadHostSpeedData(); - ui.addNotification(null, E('p',_('Speed limit updated'))); - ui.hideModal(); - } catch (e) { - ui.addNotification(null, E('p', _('Error: ') + e.message)); - } - })}, _('Save')) - ]) - ]); - }, - - validateData: function(value, type) { - if (typeof value !== 'string') return false; - const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/; - const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/i; - const macRegex = /^([0-9A-Fa-f]{2}([-:]))([0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{12})$/i; - return (type === 'ipv4') ? ipv4Regex.test(value) : (type === 'ipv6') ? ipv6Regex.test(value) : macRegex.test(value); - }, - - createAddControls: function(type, placeholder) { - const input = E('input', { - type: 'text', - class: 'cbi-input-text control-input', - style: (type === 'ipv6') ? 'width:320px' : 'width:180px', - placeholder: _(placeholder) - }); - const addBtn = E('button', { - class: 'btn cbi-button cbi-button-add', - disabled: true - }, [ - E('span', { 'class': 'btn-icon' }, '➕'), - E('span', {}, ' ' + _('Add')) - ]); - const refreshBtn = E('button', { - class: 'btn cbi-button cbi-button-action', - click: () => this.loadHostSpeedData() - }, [ - E('span', { 'class': 'btn-icon' }, '🔄'), - E('span', {}, ' ' + _('Refresh')) - ]); - - input.addEventListener('input', () => { addBtn.disabled = (input.value.trim() === ''); }); - addBtn.addEventListener('click', ui.createHandlerFn(this, async () => { - const value = input.value.trim(); - if (!this.validateData(value, type)) { - return ui.addNotification(null, E('p', _('Data format error'))); - } - try { - await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'add', value]); - this.loadHostSpeedData(); - ui.addNotification(null, E('p',_('Updated successfully!'))); - input.value = ''; - addBtn.disabled = true; - } catch (e) { - ui.addNotification(null, E('p', _('Error: ') + e.message)); - } - })); - - return E('div', { 'class': 'display-controls' }, [ - E('div', { 'class': 'control-group' }, [ - E('span', { 'class': 'control-icon' }, '🖥️'), - E('label', { 'class': 'control-label' }, _('Add Host:')), - input - ]), - E('div', { 'class': 'control-buttons' }, [ - addBtn, - refreshBtn, - E('div', { 'class': 'control-group status-group' }, [ - E('span', { 'class': 'control-icon' }, '🕐'), - E('span', { 'id': 'display-last-updated', 'class': 'last-updated-text' }, _('Ready')) - ]) - ]) - ]); - }, - - initializeUI: function() { - if (window.echarts) { - var self = this; - ['ipv4', 'ipv6', 'mac'].forEach(function(type) { - var dlChartEl = document.getElementById(type + '-download-speed-line-chart'); - var ulChartEl = document.getElementById(type + '-upload-speed-line-chart'); - if (!dlChartEl || !ulChartEl) return; - - var baseChartOption = { - tooltip: { - trigger: 'axis', - formatter: function (params) { - if (!params || params.length === 0) { - return null; - } - var tooltipContent = params[0].axisValueLabel + '
'; - params.sort(function(a, b) { return b.value - a.value; }); - params.forEach(function(item) { - if (item.value > 0) { - tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mBps'.format(item.value) + '
'; - } - }); - return tooltipContent; - } - }, - grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true }, - xAxis: { type: 'category', boundaryGap: false, data: lineCategories[type] }, - yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mBps'.format(val); } } }, - series: [] - }; - - downloadLineChart[type] = echarts.init(dlChartEl); - downloadLineChart[type].setOption(baseChartOption); - - uploadLineChart[type] = echarts.init(ulChartEl); - uploadLineChart[type].setOption(baseChartOption); - }); - - // 添加窗口大小变化监听器,使图表能够响应式调整 - if (!resizeListenerAdded) { - var resizeTimer = null; - var resizeHandler = function() { - // 使用防抖,避免频繁触发 resize - if (resizeTimer) { - clearTimeout(resizeTimer); - } - resizeTimer = setTimeout(function() { - // 调整折线图大小 - ['ipv4', 'ipv6', 'mac'].forEach(function(type) { - if (downloadLineChart[type]) { - downloadLineChart[type].resize(); - } - if (uploadLineChart[type]) { - uploadLineChart[type].resize(); - } - }); - // 调整饼图大小 - Object.keys(chartRegistry).forEach(function(chartId) { - if (chartRegistry[chartId]) { - chartRegistry[chartId].resize(); - } - }); - }, 200); - }; - - window.addEventListener('resize', resizeHandler); - resizeListenerAdded = true; - } - - this.pollData(); - } else { - setTimeout(this.initializeUI.bind(this), 50); - } - }, - - // --- Main Render Function (New) --- - - render: function() { - var self = this; - - const createTab = (type, title, placeholder) => { - return E('div', { 'class': 'cbi-section', 'data-tab': type, 'data-tab-title': _(title) }, [ - E('div', { 'class': 'dashboard-container' }, [ - E('div', { 'class': 'line-chart-row' }, [ - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Download Speed')]), - E('div', { id: type + '-download-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Upload Speed')]), - E('div', { id: type + '-upload-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]) - ]), - E('div', { 'class': 'kpi-row' }, [ - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-host-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Hosts')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ]) - ]), - E('div', { 'class': 'chart-grid' }, [ - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Speed / Host')]), E('div', { id: type + '-tx-rate-pie', style: 'width:100%; height:300px;' }) ]), - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Speed / Host')]), E('div', { id: type + '-rx-rate-pie', style: 'width:100%; height:300px;' }) ]), - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Total')]), E('div', { id: type + '-tx-volume-pie', style: 'width:100%; height:300px;' }) ]), - E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Total')]), E('div', { id: type + '-rx-volume-pie', style: 'width:100%; height:300px;' }) ]) - ]) - ]), - E('table', { 'class': 'table', 'id': type + '-speed-data' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🖥️'), ' ', _('Host') ]), - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '👤'), ' ', _('Hostname') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download Total') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download Packets') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload Total') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload Packets') ]), - E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚙️'), ' ', _('Actions') ]) - ]), - E('tr', { 'class': 'tr placeholder' }, [ E('td', { 'class': 'td', 'colspan': '9' }, [ E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) ]) ]) - ]), - self.createAddControls(type, placeholder) - ]); - }; - - var tabContainer = E('div', {}, [ - createTab('ipv4', 'IPv4', 'Please enter a valid IPv4 address'), - createTab('ipv6', 'IPv6', 'Please enter a valid IPv6 address'), - createTab('mac', 'MAC', 'Please enter a valid MAC address') - ]); - - var node = E([], [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }), - E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }), - E('div', { 'class': 'l7-view-container' }, [ - E('h2', [ _('Auth User Speed Monitor') ]), - tabContainer - ]) - ]); - - ui.tabs.initTabGroup(tabContainer.childNodes); - - setTimeout(() => this.initializeUI(), 0); - - return node; - }, - - handleSave: null, - handleSaveApply: null, - handleReset: null -}); diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js index 6be79ea1a..445daf4bc 100644 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js +++ b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/wifidogx/status.js @@ -266,14 +266,14 @@ return view.extend({ } } + // Initial update + updateStatus(); + // Poll status every 5 seconds L.Poll.add(function() { return updateStatus(); }, 5); - // Initial update - updateStatus(); - return container; }, diff --git a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js b/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js deleted file mode 100644 index 781382bb5..000000000 --- a/luci-app-apfree-wifidog/htdocs/luci-static/resources/view/xdpi/l7.js +++ /dev/null @@ -1,695 +0,0 @@ -'use strict'; -'require view'; -'require fs'; -'require ui'; -'require poll'; -'require rpc'; -'require dom'; - -var chartRegistry = {}; -var downloadLineChart, uploadLineChart; - -// Data structures for stacked line charts -var lineCategories = []; -var downloadSeriesData = {}; -var uploadSeriesData = {}; - -// Color palette for chart series -var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']; - -var currentSortInfo = { - table: null, - column: null, - reverse: false -}; -var sidLookupTable = {}; -var isPaused = false; -var lastUpdated = null; -var pollActive = false; -var lastSIDData = null; -var resizeListenerAdded = false; -var resizeTimer = null; - -// Pre-fill with 60 empty points for a smooth start -for (var i = 0; i < 60; i++) { - lineCategories.push(''); -} - -// Helper to convert hex to rgba -function hexToRgba(hex, opacity) { - var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); - return result ? - 'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' : - null; -}; - -return view.extend({ - load: function() { - return Promise.all([ - this.loadSIDData(), - this.loadL7ProtoData() - ]); - }, - - showError: function(message) { - var errorEl = document.getElementById('l7-error-message'); - if (errorEl) { - errorEl.textContent = message; - errorEl.style.display = 'block'; - } - }, - - hideError: function() { - var errorEl = document.getElementById('l7-error-message'); - if (errorEl) { - errorEl.style.display = 'none'; - } - }, - - loadSIDData: function() { - var self = this; - return fs.exec_direct('/usr/bin/aw-bpfctl', ['sid', 'json'], 'json').then(function(result) { - self.hideError(); - lastSIDData = result; - return result; - }).catch(function(error) { - console.error('Error loading SID data:', error); - self.showError(_('Error loading SID data: %s').format(error.message)); - return { status: 'error', data: [] }; - }); - }, - - loadL7ProtoData: function() { - var self = this; - return fs.exec_direct('/usr/bin/aw-bpfctl', ['l7', 'json'], 'json').then(function(result) { - self.hideError(); - return result; - }).catch(function(error) { - console.error('Error loading L7 protocol data:', error); - self.showError(_('Error loading L7 protocol data: %s').format(error.message)); - return { status: 'error', data: [] }; - }); - }, - - updateStackedLineCharts: function(perServiceDownload, perServiceUpload) { - var now = new Date().toLocaleTimeString(); - lineCategories.push(now); - lineCategories.shift(); - - var processChartData = function(seriesData, perServiceData) { - var allServices = Object.keys(seriesData); - Object.keys(perServiceData).forEach(function(service) { - if (allServices.indexOf(service) === -1) { - allServices.push(service); - } - }); - - allServices.forEach(function(service) { - if (!seriesData[service]) { - seriesData[service] = Array(59).fill(0); - } - var rate = perServiceData[service] || 0; - seriesData[service].push(rate); - seriesData[service].shift(); - }); - - return Object.keys(seriesData).map(function(service, index) { - var color = colorPalette[index % colorPalette.length]; - return { - name: service, - type: 'line', - stack: 'Total', - smooth: true, - lineStyle: { width: 1, color: color }, - showSymbol: false, - itemStyle: { color: color }, - areaStyle: { - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ - { offset: 0, color: hexToRgba(color, 0.5) }, - { offset: 1, color: hexToRgba(color, 0) } - ]) - }, - data: seriesData[service] - }; - }); - }; - - var downloadChartSeries = processChartData(downloadSeriesData, perServiceDownload); - var uploadChartSeries = processChartData(uploadSeriesData, perServiceUpload); - - var legendData = downloadChartSeries.map(function(s) { return s.name; }); - - if (downloadLineChart) { - downloadLineChart.setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: downloadChartSeries, - xAxis: { data: lineCategories } - }); - } - - if (uploadLineChart) { - uploadLineChart.setOption({ - legend: { data: legendData, type: 'scroll', top: 0, left: 'center' }, - series: uploadChartSeries, - xAxis: { data: lineCategories } - }); - } - }, - - pie: function(id, data, valueFormatter) { - var total = data.reduce(function(n, d) { return n + d.value; }, 0); - - data.sort(function(a, b) { return b.value - a.value; }); - - if (total === 0) { - data = [{ value: 1, color: '#cccccc', name: _('no traffic') }]; - } - - data.forEach(function(d, i) { - if (!d.color) { - var hue = (i * 137.508) % 360; - d.color = 'hsl(' + hue + ', 75%, 55%)'; - } - }); - - var option = { - tooltip: { - trigger: 'item', - formatter: function(params) { - if (valueFormatter) { - // 将 ECharts params 对象转换为自定义格式 - return valueFormatter({ - name: params.name, - value: params.value, - percent: params.percent.toFixed(2) - }); - } - return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)'; - } - }, - series: [{ - type: 'pie', - radius: ['25%', '80%'], - avoidLabelOverlap: false, - padAngle: 10, - itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 }, - label: { show: false, position: 'center' }, - emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } }, - labelLine: { show: false }, - data: data.map(function(d) { - return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } }; - }) - }] - }; - - var dom = typeof id === 'string' ? document.getElementById(id) : id; - - if (!chartRegistry[id]) { - chartRegistry[id] = echarts.init(dom); - } - - chartRegistry[id].setOption(option, true); - - return chartRegistry[id]; - }, - - sortTable: function(table, column) { - var tbody = table.querySelector('tbody'); - if (!tbody) return; - var rows = Array.from(tbody.querySelectorAll('tr:not(.table-titles):not(.placeholder)')); - var reverse = (currentSortInfo.table === table && currentSortInfo.column === column) ? !currentSortInfo.reverse : false; - - table.querySelectorAll('th').forEach(function(th) { - th.classList.remove('th-sort-asc', 'th-sort-desc'); - }); - - var th = table.querySelector('th:nth-child(' + (column + 1) + ')'); - th.classList.add(reverse ? 'th-sort-desc' : 'th-sort-asc'); - - rows.sort(function(row1, row2) { - var a = row1.cells[column].getAttribute('data-value') || row1.cells[column].textContent; - var b = row2.cells[column].getAttribute('data-value') || row2.cells[column].textContent; - - if (!isNaN(a) && !isNaN(b)) { a = Number(a); b = Number(b); } - - if (a < b) return reverse ? 1 : -1; - if (a > b) return reverse ? -1 : 1; - return 0; - }); - - currentSortInfo.table = table; - currentSortInfo.column = column; - currentSortInfo.reverse = reverse; - - rows.forEach(function(row) { tbody.removeChild(row); }); - rows.forEach(function(row) { tbody.appendChild(row); }); - }, - - formatMbps: function(bits) { - if (typeof bits !== 'number') return '0.00 Mbps'; - return (bits / 1024 / 1024).toFixed(2) + ' Mbps'; - }, - - formatMB: function(bytes) { - if (typeof bytes !== 'number') return '0.00 MB'; - return (bytes / 1024 / 1024).toFixed(2) + ' MB'; - }, - - renderSIDData: function(data) { - var rows = []; - var txRateData = [], rxRateData = []; - var txVolumeData = [], rxVolumeData = []; - var tx_rate_total = 0, rx_rate_total = 0; - var tx_bytes_total = 0, rx_bytes_total = 0; - var perServiceTxRate = {}; - var perServiceRxRate = {}; - var self = this; - var allItems = []; - - if (data && data.status === 'success' && Array.isArray(data.data)) { - allItems = data.data; - var listSizeEl = document.getElementById('sid-size-select'); - var listSize = listSizeEl ? parseInt(listSizeEl.value, 10) : 10; - - var activeConnections = allItems.filter(function(item) { return item.incoming.rate > 0 || item.outgoing.rate > 0; }); - var inactiveConnections = allItems.filter(function(item) { return item.incoming.rate === 0 && item.outgoing.rate === 0; }); - - activeConnections.sort(function(a, b) { return (b.incoming.rate + b.outgoing.rate) - (a.incoming.rate + a.outgoing.rate); }); - inactiveConnections.sort(function(a, b) { return b.incoming.total_bytes - a.incoming.total_bytes; }); - - var displayData = activeConnections; - if (displayData.length < listSize) { - displayData = displayData.concat(inactiveConnections.slice(0, listSize - displayData.length)); - } - - if (displayData.length > listSize) { - displayData = displayData.slice(0, listSize); - } - - displayData.forEach(function(item) { - var domainOrL7Proto = 'unknown'; - var lookupInfo = sidLookupTable[item.sid]; - - if (lookupInfo) { - domainOrL7Proto = lookupInfo.name; - } else if (item.sid_type === 'Domain' && item.domain && item.domain !== 'unknown') { - domainOrL7Proto = item.domain; - } else if (item.sid_type === 'L7' && item.l7_proto_desc && item.l7_proto_desc !== 'unknown') { - domainOrL7Proto = item.l7_proto_desc; - } - - // 判断连接是否活跃 - var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0; - var activityIcon = isActive ? '🟢' : '⚪'; - - rows.push([ - E('span', { 'class': 'sid-cell' }, [ - E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon), - E('span', {}, ' ' + item.sid) - ]), - E('span', { 'class': 'protocol-cell' }, [ - E('span', { 'class': 'protocol-icon' }, '🌐'), - E('span', {}, ' ' + domainOrL7Proto) - ]), - [ item.incoming.rate, E('span', { 'class': 'speed-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.incoming.rate)) - ])], - [ item.incoming.total_bytes, E('span', { 'class': 'volume-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes)) - ])], - [ item.incoming.total_packets, E('span', { 'class': 'packet-cell download' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets)) - ])], - [ item.outgoing.rate, E('span', { 'class': 'speed-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.outgoing.rate)) - ])], - [ item.outgoing.total_bytes, E('span', { 'class': 'volume-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes)) - ])], - [ item.outgoing.total_packets, E('span', { 'class': 'packet-cell upload' }, [ - E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets)) - ])] - ]); - - txRateData.push({ value: item.incoming.rate, label: domainOrL7Proto }); - rxRateData.push({ value: item.outgoing.rate, label: domainOrL7Proto }); - txVolumeData.push({ value: item.incoming.total_bytes, label: domainOrL7Proto }); - rxVolumeData.push({ value: item.outgoing.total_bytes, label: domainOrL7Proto }); - - perServiceTxRate[domainOrL7Proto] = (perServiceTxRate[domainOrL7Proto] || 0) + item.incoming.rate; - perServiceRxRate[domainOrL7Proto] = (perServiceRxRate[domainOrL7Proto] || 0) + item.outgoing.rate; - }); - - allItems.forEach(function(item) { - tx_rate_total += item.incoming.rate; - rx_rate_total += item.outgoing.rate; - tx_bytes_total += item.incoming.total_bytes; - rx_bytes_total += item.outgoing.total_bytes; - }); - } - - this.updateStackedLineCharts(perServiceTxRate, perServiceRxRate); - - var table = document.getElementById('sid-data'); - cbi_update_table('#sid-data', rows, E('em', _('No data recorded yet.'))); - - var headers = table.querySelectorAll('th'); - - if (!table.hasAttribute('data-sort-initialized')) { - headers.forEach(function(header, index) { - header.style.cursor = 'pointer'; - header.addEventListener('click', function() { self.sortTable(table, index); }); - }); - table.setAttribute('data-sort-initialized', 'true'); - } - - table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) { - if (!rows[rowIndex]) return; - Array.from(row.cells).forEach(function(cell, cellIndex) { - if (Array.isArray(rows[rowIndex][cellIndex])) { - cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]); - } - }); - }); - - this.pie('sid-tx-rate-pie', txRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; }); - this.pie('sid-rx-rate-pie', rxRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; }); - this.pie('sid-tx-volume-pie', txVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; }); - this.pie('sid-rx-volume-pie', rxVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; }); - - var sidTotalEl = document.getElementById('sid-total-val'); - if(sidTotalEl) sidTotalEl.textContent = allItems.length; - - var txRateEl = document.getElementById('sid-tx-rate-val'); - if(txRateEl) txRateEl.textContent = '%1024.2mbps'.format(tx_rate_total); - - var rxRateEl = document.getElementById('sid-rx-rate-val'); - if(rxRateEl) rxRateEl.textContent = '%1024.2mbps'.format(rx_rate_total); - - var txVolEl = document.getElementById('sid-tx-volume-val'); - if(txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total); - - var rxVolEl = document.getElementById('sid-rx-volume-val'); - if(rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total); - - lastUpdated = new Date(); - var timestampEl = document.getElementById('last-updated'); - if (timestampEl) { - timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString()); - } - }, - - renderL7ProtoData: function(data) { - var rows = []; - var self = this; - - sidLookupTable = {}; - - if (data && data.status === 'success' && data.data) { - if (Array.isArray(data.data.protocols)) { - data.data.protocols.forEach(function(item) { - sidLookupTable[item.sid] = { type: 'protocol', name: item.protocol }; - rows.push([ - E('span', { 'class': 'id-cell' }, item.id), - E('span', { 'class': 'protocol-cell' }, [ - E('span', { 'class': 'protocol-icon l7' }, '🔌'), - E('span', {}, ' ' + item.protocol) - ]), - E('span', { 'class': 'sid-cell' }, item.sid) - ]); - }); - } - - if (Array.isArray(data.data.domains)) { - data.data.domains.forEach(function(item) { - sidLookupTable[item.sid] = { type: 'domain', name: item.domain }; - rows.push([ - E('span', { 'class': 'id-cell' }, item.id), - E('span', { 'class': 'protocol-cell' }, [ - E('span', { 'class': 'protocol-icon domain' }, '🌍'), - E('span', {}, ' ' + item.domain) - ]), - E('span', { 'class': 'sid-cell' }, item.sid) - ]); - }); - } - } - - var table = document.getElementById('l7proto-data'); - var headers = table.querySelectorAll('th'); - - if (!table.hasAttribute('data-sort-initialized')) { - headers.forEach(function(header, index) { - header.style.cursor = 'pointer'; - header.addEventListener('click', function() { self.sortTable(table, index); }); - }); - table.setAttribute('data-sort-initialized', 'true'); - } - - cbi_update_table('#l7proto-data', rows, E('em', _('No data recorded yet.'))); - }, - - pollL7Data: function() { - if (pollActive) return; - - var self = this; - pollActive = true; - - self.loadL7ProtoData().then(function(l7data) { - self.renderL7ProtoData(l7data); - return self.loadSIDData(); - }).then(function(sidData){ - self.renderSIDData(sidData); - }); - - poll.add(function() { - if (isPaused) return Promise.resolve(); - - return self.loadL7ProtoData().then(function(data) { - self.renderL7ProtoData(data); - }).then(function() { - return self.loadSIDData().then(function(data) { - self.renderSIDData(data); - }); - }); - }, 5); - }, - - initializeUI: function() { - if (window.echarts) { - var dlChartEl = document.getElementById('download-speed-line-chart'); - var ulChartEl = document.getElementById('upload-speed-line-chart'); - if (!dlChartEl || !ulChartEl) return; - - var baseChartOption = { - tooltip: { - trigger: 'axis', - formatter: function (params) { - if (!params || params.length === 0) { - return null; - } - var tooltipContent = params[0].axisValueLabel + '
'; - params.sort(function(a, b) { return b.value - a.value; }); - params.forEach(function(item) { - if (item.value > 0) { - tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mbps'.format(item.value) + '
'; - } - }); - return tooltipContent; - } - }, - grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true }, - xAxis: { type: 'category', boundaryGap: false, data: lineCategories }, - yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mbps'.format(val); } } }, - series: [] - }; - - - downloadLineChart = echarts.init(dlChartEl); - downloadLineChart.setOption(baseChartOption); - - uploadLineChart = echarts.init(ulChartEl); - uploadLineChart.setOption(baseChartOption); - - // 添加窗口大小变化监听器,使图表能够响应式调整 - if (!resizeListenerAdded) { - var resizeTimer = null; - var resizeHandler = function() { - // 使用防抖,避免频繁触发 resize - if (resizeTimer) { - clearTimeout(resizeTimer); - } - resizeTimer = setTimeout(function() { - // 调整折线图大小 - if (downloadLineChart) { - downloadLineChart.resize(); - } - if (uploadLineChart) { - uploadLineChart.resize(); - } - // 调整饼图大小 - Object.keys(chartRegistry).forEach(function(chartId) { - if (chartRegistry[chartId]) { - chartRegistry[chartId].resize(); - } - }); - }, 200); - }; - - window.addEventListener('resize', resizeHandler); - resizeListenerAdded = true; - } - - this.pollL7Data(); - } else { - setTimeout(this.initializeUI.bind(this), 50); - } -}, render: function() { - var self = this; - - var controls = E('div', { 'class': 'l7-controls' }, [ - E('div', { 'class': 'l7-controls-left' }, [ - E('div', { 'class': 'control-group' }, [ - E('span', { 'class': 'control-icon' }, '📊'), - E('label', { 'for': 'sid-size-select', 'class': 'control-label' }, _('Show entries:')), - E('select', { - 'id': 'sid-size-select', - 'class': 'cbi-input-select', - 'change': ui.createHandlerFn(this, function() { - if (lastSIDData) { - self.renderSIDData(lastSIDData); - } - }) - }, [ - E('option', { 'value': '10' }, '10'), - E('option', { 'value': '15' }, '15'), - E('option', { 'value': '20' }, '20'), - E('option', { 'value': '25' }, '25'), - E('option', { 'value': '50' }, '50') - ]) - ]) - ]), - E('div', { 'class': 'l7-controls-right' }, [ - E('div', { 'class': 'control-group' }, [ - E('span', { 'class': 'control-icon' }, '🕐'), - E('span', { 'id': 'last-updated', 'class': 'last-updated-text' }, _('Last updated: never')) - ]), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'id': 'pause-resume-btn', - 'click': function(ev) { - isPaused = !isPaused; - var btn = ev.target; - if (isPaused) { - btn.innerHTML = '▶️ ' + _('Resume'); - btn.classList.remove('cbi-button-action'); - btn.classList.add('cbi-button-positive'); - } else { - btn.innerHTML = '⏸️ ' + _('Pause'); - btn.classList.remove('cbi-button-positive'); - btn.classList.add('cbi-button-action'); - } - } - }, [ - E('span', { 'class': 'btn-icon' }, '⏸️'), - E('span', {}, ' ' + _('Pause')) - ]) - ]) - ]); - - var tabContainer = E('div', {}, [ - E('div', { 'class': 'cbi-section', 'data-tab': 'sid', 'data-tab-title': _('L7 SID Data') }, [ - E('div', { 'class': 'dashboard-container' }, [ - E('div', { 'class': 'line-chart-row' }, [ - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Download Speed')]), - E('div', { id: 'download-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Real-time Upload Speed')]), - E('div', { id: 'upload-speed-line-chart', style: 'width: 100%; height: 350px;' }) - ]) - ]), - E('div', { 'class': 'kpi-row' }, [ - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-total-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('L7 Protocol Data')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]), - E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ]) - ]), - E('div', { 'class': 'chart-grid' }, [ - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Download Speed / SID')]), - E('div', { id: 'sid-tx-rate-pie', style: 'width: 100%; height: 300px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Upload Speed / SID')]), - E('div', { id: 'sid-rx-rate-pie', style: 'width: 100%; height: 300px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Download Total')]), - E('div', { id: 'sid-tx-volume-pie', style: 'width: 100%; height: 300px;' }) - ]), - E('div', { 'class': 'chart-card' }, [ - E('h4', [_('Upload Total')]), - E('div', { id: 'sid-rx-volume-pie', style: 'width: 100%; height: 300px;' }) - ]) - ]) - ]), - E('table', { 'class': 'table', 'id': 'sid-data' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🆔'), ' ', _('SID') ]), - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed (Bit/s)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download (Bytes)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download (Packets)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed (Bit/s)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload (Bytes)') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload (Packets)') ]) - ]), - E('tr', { 'class': 'tr placeholder' }, [ - E('td', { 'class': 'td', 'colspan': '8' }, [ - E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) - ]) - ]) - ]), - controls - ]), - E('div', { 'class': 'cbi-section', 'data-tab': 'l7proto', 'data-tab-title': _('L7 Protocol Data') }, [ - E('table', { 'class': 'table', 'id': 'l7proto-data' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]), - E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]), - E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ]) - ]), - E('tr', { 'class': 'tr placeholder' }, [ - E('td', { 'class': 'td', 'colspan': '3' }, [ - E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) - ]) - ]) - ]) - ]) - ]); - - var node = E([], [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }), - E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }), - - E('div', { 'class': 'l7-view-container' }, [ - E('h2', [ _('L7 Data Monitor') ]), - E('div', { 'id': 'l7-error-message' }), - tabContainer - ]) - ]); - - ui.tabs.initTabGroup(tabContainer.childNodes); - - setTimeout(this.initializeUI.bind(this), 0); - - return node; - }, - - handleSave: null, - handleSaveApply: null, - handleReset: null -}); \ No newline at end of file diff --git a/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po b/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po index 868c0241d..d44cd5fec 100644 --- a/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po +++ b/luci-app-apfree-wifidog/po/zh_Hans/apfree-wifidog.po @@ -15,6 +15,9 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Settings" +msgstr "设置" + msgid "apfree-wifidog" msgstr "门户认证" @@ -165,6 +168,9 @@ msgstr "信任的通配符域名" msgid "The trusted wildcard domains of the gateway." msgstr "网关的信任通配符域名" +msgid "Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf." +msgstr "依赖 aw-bpf 的 DNS XDP(dns_ringbuf_portal)。格式如 .example.com;解析到的 IP 写入门户防火墙 inet wifidogx,不会写入 inet awbpf。" + msgid "App White List" msgstr "应用白名单" @@ -228,6 +234,12 @@ msgstr "启用防NAT" msgid "Enable Anti NAT devices." msgstr "启用防NAT设备" +msgid "Privileged Ops Secret" +msgstr "特权管控秘钥" + +msgid "Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices." +msgstr "AWAS云端管理平台动态授权远程OTA升级、重启及系统配置变更所需的本地安全秘钥。生产环境请修改默认值。" + msgid "TTL Value" msgstr "TTL值" @@ -1093,3 +1105,48 @@ msgstr "MQTT认证的用户名。" msgid "The password for MQTT authentication." msgstr "MQTT认证的密码。" +msgid "Add Authentication Server" +msgstr "添加认证服务器" + +msgid "Enter a unique name for this authentication server profile." +msgstr "请输入认证服务器配置文件的唯一名称。" + +msgid "Add Long Connection Profile" +msgstr "添加长连接配置" + +msgid "Enter a unique name for this long connection profile." +msgstr "请输入长连接配置文件的唯一名称。" + +msgid "Rename Authentication Server" +msgstr "重命名认证服务器" + +msgid "Enter a new unique name for this authentication server profile." +msgstr "请输入认证服务器配置文件的新名称。" + +msgid "Rename Long Connection Profile" +msgstr "重命名长连接配置" + +msgid "Enter a new unique name for this long connection profile." +msgstr "请输入长连接配置文件的新名称。" + +msgid "Please enter a name." +msgstr "请输入名称。" + +msgid "The name \"%s\" already exists in Auth Server profiles. Please choose a different name." +msgstr "名称\"%s\"已存在于认证服务器配置中,请选择其他名称。" + +msgid "The name \"%s\" already exists in Long Connection profiles. Please choose a different name." +msgstr "名称\"%s\"已存在于长连接配置中,请选择其他名称。" + +msgid "Created new authentication server profile \"%s\"." +msgstr "已创建新的认证服务器配置\"%s\"。" + +msgid "Created new long connection profile \"%s\"." +msgstr "已创建新的长连接配置\"%s\"。" + +msgid "Renamed authentication server profile from \"%s\" to \"%s\"." +msgstr "已将认证服务器配置从\"%s\"重命名为\"%s\"。" + +msgid "Renamed long connection profile from \"%s\" to \"%s\"." +msgstr "已将长连接配置从\"%s\"重命名为\"%s\"。" + diff --git a/luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh b/luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh deleted file mode 100644 index cf3a7cbdd..000000000 --- a/luci-app-apfree-wifidog/root/etc/uci-defaults/luci-app-apfree-wifidog.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -[ -f "/etc/config/hostnames" ] || { - echo 'config hostname' > /etc/config/hostnames -} - -exit 0 diff --git a/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx b/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx index ff8d2e55c..9c4794010 100755 --- a/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx +++ b/luci-app-apfree-wifidog/root/usr/libexec/rpcd/luci.wifidogx @@ -70,7 +70,7 @@ case "$1" in json_init json_add_object "result" json_add_string "status" "fail" - json_add_string "message" "WAN interface not found. Available interfaces: $(ls /sys/class/net/ | tr '\n' ' ')" + json_add_string "message" "WAN interface not found" json_close_object json_dump fi diff --git a/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json b/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json index ba3b75c70..2add1a4d8 100644 --- a/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json +++ b/luci-app-apfree-wifidog/root/usr/share/luci/menu.d/luci-app-apfree-wifidog.json @@ -3,56 +3,27 @@ "title": "apfree-wifidog", "order": 1, "action": { - "type": "view", - "path": "wifidogx" + "type": "firstchild", + "recurse": true }, "depends": { "acl": [ "luci-app-apfree-wifidog" ] } }, - "admin/QoS": { - "title": "QoS", - "order": 20, - "action": { - "type": "firstchild", - "recurse": true - } - }, - "admin/QoS/wifidogx": { - "title": "Auth User", - "order": 2, - "action": { - "type": "alias", - "path": "admin/QoS/wifidogx/display" - }, - "depends": { - "acl": [ - "luci-app-apfree-wifidog" - ] - } - }, - "admin/QoS/wifidogx/display": { - "title": "Display", + "admin/services/wifidogx/config": { + "title": "Settings", "order": 10, "action": { "type": "view", - "path": "wifidogx/display" + "path": "wifidogx" } }, - "admin/QoS/wifidogx/status": { + "admin/services/wifidogx/status": { "title": "Status", "order": 20, "action": { "type": "view", "path": "wifidogx/status" } - }, - "admin/QoS/xdpi": { - "title": "L7 application", - "order": 30, - "action": { - "type": "view", - "path": "xdpi/l7" - } } } diff --git a/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json b/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json index 78975c66c..115acc1ae 100644 --- a/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json +++ b/luci-app-apfree-wifidog/root/usr/share/rpcd/acl.d/luci-app-apfree-wifidog.json @@ -8,14 +8,12 @@ }, "file": { "/etc/init.d/wifidogx": [ "exec" ], - "/usr/bin/wdctlx": [ "exec" ], - "/usr/bin/aw-bpfctl": [ "exec" ], - "/usr/bin/awk": [ "exec" ] + "/usr/bin/wdctlx": [ "exec" ] }, - "uci": ["wifidogx", "hostnames", "network"] + "uci": ["wifidogx", "network"] }, "write": { - "uci": ["wifidogx", "hostnames"], + "uci": ["wifidogx"], "file": { "/etc/wifidogx/*": [ "write" ] } diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index b70561826..961c0b53d 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.9.9 -PKG_RELEASE:=263 +PKG_RELEASE:=264 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index ab5bce439..76df7aa55 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -1483,23 +1483,21 @@ acl_app() { esac } } - dns_cache_key="${dns_mode}_${remote_dns}_${v2ray_dns_mode:-none}_${remote_dns_client_ip:-0}_${remote_fakedns:-0}_${remote_rewrite_ttl:-30}" - ([ "$v2ray_dns_mode" = "doh" ] || [ "$v2ray_dns_mode" = "http3" ]) && { - dns_cache_key="${dns_mode}_${remote_dns_doh:-https://1.1.1.1/dns-query}_${v2ray_dns_mode:-doh}_${remote_dns_client_ip:-0}_${remote_fakedns:-0}_${remote_rewrite_ttl:-30}" - } + + local dns_cache_str="${dns_mode}_${v2ray_dns_mode}_${remote_dns}_${remote_dns_doh}_${remote_dns_client_ip}_${remote_fakedns}_${remote_rewrite_ttl}" + dns_cache_key="$(echo -n "${dns_cache_str}" | md5sum | cut -d " " -f1)" if [ "$remote_fakedns" = "1" ] || ([ "$protocol" = "_shunt" ] && [ "$(config_n_get $node fakedns)" = "1" ]); then use_fakedns=1 fi run_dns() { - local _dns_port - [ -n $1 ] && _dns_port=$1 - [ -z ${_dns_port} ] && { + local dns_fwd_port=$1 + [ -z "$dns_fwd_port" ] && { dns_port=$(get_new_port $(expr $dns_port + 1)) - _dns_port=$dns_port + dns_fwd_port=$dns_port if [ "$dns_mode" = "dns2socks" ]; then - run_dns2socks flag=acl_${sid} socks_address=127.0.0.1 socks_port=$socks_port listen_address=0.0.0.0 listen_port=${_dns_port} dns=$remote_dns cache=1 + run_dns2socks flag=acl_${sid} socks_address=127.0.0.1 socks_port=$socks_port listen_address=0.0.0.0 listen_port=$dns_fwd_port dns=$remote_dns cache=1 elif [ "$dns_mode" = "sing-box" ] || [ "$dns_mode" = "xray" ]; then config_file=$TMP_ACL_PATH/${node}_SOCKS_${socks_port}_DNS.json remote_dns_doh=${remote_dns_doh:-https://1.1.1.1/dns-query} @@ -1507,10 +1505,15 @@ acl_app() { [ "${dns_mode}" = "sing-box" ] && type="singbox" dnsmasq_filter_proxy_ipv6=0 remote_dns_query_strategy="UseIP" - [ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4" - run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file + [ "$filter_proxy_ipv6" = "1" ] && { + remote_dns_query_strategy="UseIPv4" + set_cache_var "node_${node}_${dns_cache_key}_ipv6" "0" + } + run_${type} flag=acl_${sid} type=$dns_mode dns_socks_address=127.0.0.1 dns_socks_port=$socks_port dns_listen_port=$dns_fwd_port \ + remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns} remote_dns_doh="${remote_dns_doh}" \ + remote_dns_query_strategy=${remote_dns_query_strategy} remote_dns_client_ip=${remote_dns_client_ip} config_file=$config_file fi - set_cache_var "node_${node}_$(echo -n "${dns_cache_key}" | md5sum | cut -d " " -f1)" "${_dns_port}" + set_cache_var "node_${node}_${dns_cache_key}" "$dns_fwd_port" } [ "$dns_shunt" = "chinadns-ng" ] && [ -n "$(first_type chinadns-ng)" ] && { @@ -1539,7 +1542,7 @@ acl_app() { _flag="$sid" \ _listen_port=${chinadns_port} \ _dns_local=${_chinadns_local_dns} \ - _dns_trust=127.0.0.1#${_dns_port} \ + _dns_trust=127.0.0.1#${dns_fwd_port} \ _no_ipv6_trust=${filter_proxy_ipv6} \ _use_direct_list=${use_direct_list} \ _use_proxy_list=${use_proxy_list} \ @@ -1556,33 +1559,33 @@ acl_app() { use_default_dns="chinadns_ng" } + ([ "$filter_proxy_ipv6" = "1" ] && [ "$dnsmasq_filter_proxy_ipv6" = "1" ]) && { + dnsmasq_filter_proxy_ipv6=$(get_cache_var "node_${node}_${dns_cache_key}_ipv6") + dnsmasq_filter_proxy_ipv6=${dnsmasq_filter_proxy_ipv6:-1} + } + dnsmasq_port=$(get_new_port $(expr $dnsmasq_port + 1)) local dnsmasq_conf=${acl_path}/dnsmasq.conf local dnsmasq_conf_path=${acl_path}/dnsmasq.d lua $APP_PATH/helper_dnsmasq.lua add_rule -FLAG ${sid} -TMP_DNSMASQ_PATH ${dnsmasq_conf_path} -DNSMASQ_CONF_FILE ${dnsmasq_conf} \ -LISTEN_PORT ${dnsmasq_port} -DEFAULT_DNS ${DEFAULT_DNS} -LOCAL_DNS $LOCAL_DNS \ -USE_DIRECT_LIST "${use_direct_list}" -USE_PROXY_LIST "${use_proxy_list}" -USE_BLOCK_LIST "${use_block_list}" -USE_GFW_LIST "${use_gfw_list}" -CHN_LIST "${chn_list}" \ - -TUN_DNS "127.0.0.1#${_dns_port}" -USE_DEFAULT_DNS "${use_default_dns:-direct}" -CHINADNS_DNS ${_china_ng_listen:-0} \ + -TUN_DNS "127.0.0.1#${dns_fwd_port}" -USE_DEFAULT_DNS "${use_default_dns:-direct}" -CHINADNS_DNS ${_china_ng_listen:-0} \ -NODE $node -DEFAULT_PROXY_MODE ${tcp_proxy_mode} -NO_PROXY_IPV6 ${dnsmasq_filter_proxy_ipv6:-0} -NFTFLAG ${nftflag:-0} \ -NO_LOGIC_LOG 1 ln_run "$(first_type dnsmasq)" "dnsmasq_${sid}" "/dev/null" -C ${dnsmasq_conf} -x ${acl_path}/dnsmasq.pid set_cache_var "ACL_${sid}_dns_port" "${dnsmasq_port}" - set_cache_var "node_${node}_$(echo -n "${tcp_proxy_mode}_${dns_cache_key}" | md5sum | cut -d " " -f1)" "${dnsmasq_port}" #dhcp.leases to hosts $APP_PATH/lease2hosts.sh > /dev/null 2>&1 & } - _redir_port=$(get_cache_var "node_${node}_redir_port") - _socks_port=$(get_cache_var "node_${node}_socks_port") + local _redir_port=$(get_cache_var "node_${node}_redir_port") + local _socks_port=$(get_cache_var "node_${node}_socks_port") + local _dns_port if [ -n "${_socks_port}" ] && [ -n "${_redir_port}" ]; then socks_port=${_socks_port} node_port=${_redir_port} - _dnsmasq_port=$(get_cache_var "node_${node}_$(echo -n "${tcp_proxy_mode}_${dns_cache_key}" | md5sum | cut -d " " -f1)") - if [ -z "${_dnsmasq_port}" ]; then - _dns_port=$(get_cache_var "node_${node}_$(echo -n "${dns_cache_key}" | md5sum | cut -d " " -f1)") - run_dns ${_dns_port} - else - [ -n "${_dnsmasq_port}" ] && set_cache_var "ACL_${sid}_dns_port" "${_dnsmasq_port}" - fi + _dns_port=$(get_cache_var "node_${node}_${dns_cache_key}") + run_dns ${_dns_port} else socks_port=$(get_new_port $(expr $socks_port + 1)) set_cache_var "node_${node}_socks_port" "${socks_port}" @@ -1600,7 +1603,10 @@ acl_app() { _dns_port=$dns_port dnsmasq_filter_proxy_ipv6=0 remote_dns_query_strategy="UseIP" - [ "$filter_proxy_ipv6" = "1" ] && remote_dns_query_strategy="UseIPv4" + [ "$filter_proxy_ipv6" = "1" ] && { + remote_dns_query_strategy="UseIPv4" + set_cache_var "node_${node}_${dns_cache_key}_ipv6" "0" + } remote_dns_doh=${remote_dns_doh:-https://1.1.1.1/dns-query} _extra_param="${_extra_param} dns_listen_port=${_dns_port} remote_dns_protocol=${v2ray_dns_mode} remote_dns_udp_server=${remote_dns} remote_dns_tcp_server=${remote_dns}" _extra_param="${_extra_param} remote_dns_doh=${remote_dns_doh} remote_dns_query_strategy=${remote_dns_query_strategy} remote_fakedns=${remote_fakedns:-0} remote_dns_client_ip=${remote_dns_client_ip}" diff --git a/luci-app-passwall/root/usr/share/passwall/iptables.sh b/luci-app-passwall/root/usr/share/passwall/iptables.sh index 452f922fd..511f48b5f 100755 --- a/luci-app-passwall/root/usr/share/passwall/iptables.sh +++ b/luci-app-passwall/root/usr/share/passwall/iptables.sh @@ -417,7 +417,7 @@ load_acl() { [ "$_ipv4" != "1" ] && $ip6t_n -A PSW_DNS $(comment "$remarks") -p udp ${_ipt_source} --dport 53 -j REDIRECT --to-ports ${dns_redirect} 2>/dev/null $ipt_n -A PSW_DNS $(comment "$remarks") -p tcp ${_ipt_source} --dport 53 -j REDIRECT --to-ports ${dns_redirect} [ "$_ipv4" != "1" ] && $ip6t_n -A PSW_DNS $(comment "$remarks") -p tcp ${_ipt_source} --dport 53 -j REDIRECT --to-ports ${dns_redirect} 2>/dev/null - [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}使用与全局配置不相同节点,已将DNS强制重定向到专用 DNS 服务器。" + [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}与全局配置不同节点,DNS 重定向到专用 DNS 服务器 [${dns_redirect}]。" fi [ -n "$tcp_port" ] || [ -n "$udp_port" ] && { diff --git a/luci-app-passwall/root/usr/share/passwall/nftables.sh b/luci-app-passwall/root/usr/share/passwall/nftables.sh index 8065b8feb..e0234ee24 100755 --- a/luci-app-passwall/root/usr/share/passwall/nftables.sh +++ b/luci-app-passwall/root/usr/share/passwall/nftables.sh @@ -478,7 +478,7 @@ load_acl() { #nft "add rule $NFTABLE_NAME PSW_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" nft "add rule $NFTABLE_NAME PSW_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :${dns_redirect} comment \"$remarks\"" - [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}使用与全局配置不相同节点,已将DNS强制重定向到专用 DNS 服务器。" + [ -z "$(get_cache_var "ACL_${sid}_default")" ] && echolog " - ${msg}与全局配置不同节点,DNS 重定向到专用 DNS 服务器 [${dns_redirect}]。" fi [ -n "$tcp_port" ] || [ -n "$udp_port" ] && { diff --git a/luci-app-ss-redir/Makefile b/luci-app-ss-redir/Makefile deleted file mode 100644 index c2a8c17cf..000000000 --- a/luci-app-ss-redir/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -# This is free software, licensed under the Apache License, Version 2.0 . -# - -include $(TOPDIR)/rules.mk - -LUCI_TITLE:=LuCI Support for ss-redir -LUCI_DEPENDS:=+luci-base +ss-redir - -PKG_LICENSE:=Apache-2.0 -PKG_MAINTAINER:=Dengfeng Liu - -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-ss-redir/README.md b/luci-app-ss-redir/README.md deleted file mode 100644 index b1a434d11..000000000 --- a/luci-app-ss-redir/README.md +++ /dev/null @@ -1,42 +0,0 @@ -## luci-app-ss-redir - -该项目为ss-redir的luci配置界面 - -### 视频演示 - -
- - luci-app-ss-redir_intro_video - -
- -## 如何集成到openwrt中编译 - -1. 复制仓库中的文件到如下目录,并执行安装 - -``` -feeds/luci/applications/luci-app-ss-redir/ -./scripts/feeds install luci -a -``` - -2. 选择路径 - -`make menuconfig` - -LuCI > 3. Applications > luci-app-ss-redir - -3. 编译openwrt固件 - -``` -make -j4 -``` - -4. 单独编译 - -``` -make package/luci-app-ss-redir/compile -``` - -## 联系方式 - -QQ群:331230369 diff --git a/luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js b/luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js deleted file mode 100644 index 34fd7643f..000000000 --- a/luci-app-ss-redir/htdocs/luci-static/resources/view/ss-redir.js +++ /dev/null @@ -1,157 +0,0 @@ -'use strict'; -'require view'; -'require ui'; -'require form'; -'require rpc'; -'require uci'; -'require tools.widgets as widgets'; -'require tools.github as github'; - -var methods = [ - // aead - 'aes-128-gcm', - 'aes-192-gcm', - 'aes-256-gcm', - 'chacha20-ietf-poly1305', - 'xchacha20-ietf-poly1305', - // stream - 'table', - 'rc4', - 'rc4-md5', - 'aes-128-cfb', - 'aes-192-cfb', - 'aes-256-cfb', - 'aes-128-ctr', - 'aes-192-ctr', - 'aes-256-ctr', - 'bf-cfb', - 'camellia-128-cfb', - 'camellia-192-cfb', - 'camellia-256-cfb', - 'salsa20', - 'chacha20', - 'chacha20-ietf', -]; - -var callServiceList = rpc.declare({ - object: 'service', - method: 'list', - params: ['name'], - expect: { '': {} } -}); - -function getServiceStatus() { - return L.resolveDefault(callServiceList('ss-redir'), {}).then(function (res) { - var isRunning = false; - try { - isRunning = res['ss-redir']['instances']['instance1']['running']; - } catch (e) { } - return isRunning; - }); -} - -function renderStatus(isRunning) { - var renderHTML = ""; - var spanTemp = '%s %s'; - - if (isRunning) { - renderHTML += String.format(spanTemp, 'green', _("ss-redir "), _("running...")); - } else { - renderHTML += String.format(spanTemp, 'red', _("ss-redir "), _("not running...")); - } - - return renderHTML; -} - -return view.extend({ - load: function() { - return uci.load('kcptun'); - }, - render: function() { - var m, s, o; - - m = new form.Map('ss-redir', _('ss-redir')); - m.description = github.luci_desc('ss-redir redirect tcp service to kcptun client process.', 'liudf0716', 'ss-redir'); - - // add kcptun-client status section and option - s = m.section(form.NamedSection, '_status'); - s.anonymous = true; - s.render = function (section_id) { - L.Poll.add(function () { - return L.resolveDefault(getServiceStatus()).then(function(res) { - var view = document.getElementById("service_status"); - view.innerHTML = renderStatus(res); - }); - }); - - return E('div', { class: 'cbi-map' }, - E('fieldset', { class: 'cbi-section'}, [ - E('p', { id: 'service_status' }, - _('Collecting data ...')) - ]) - ); - } - - s = m.section(form.NamedSection, 'server', 'ss_redir'); - s.dynamic = true; - - // add two tabs - s.tab('ss-redir', _('ss-redir Settings')); - s.tab('server', _('Shadowsocks Server Settings')); - - // ss-redir settings - o = s.taboption('ss-redir', form.Flag, "enabled", _("Enable"), - _("Enable ss-redir service")); - o.rmempty = false; - - // server settings - o = s.taboption('ss-redir', form.Value, "server", _("Server Address"), - _("The address of kcptun client listening on. default: 127.0.0.1")); - o.datatype = "host"; - o.rmempty = false; - o.titleref = L.url('admin', 'vpn', 'kcptun'); - - o = s.taboption('ss-redir', form.Value, "server_port", _("Server Port"), - _("The port of kcptun client listening on. default: 12948")); - o.datatype = "port"; - o.rmempty = false; - - o = s.taboption('ss-redir', form.Value, "password", _("Password"), - _("The password of shadowsocks server")); - o.password = true; - o.rmempty = false; - - o = s.taboption('ss-redir', form.ListValue, "encrypt", _("Encryption Method"), - _("The encryption method of shadowsocks server")); - methods.forEach(function (method) { - o.value(method, method); - }); - o.rmempty = false; - - // add a textarea to show the config file content - o = s.taboption('server', form.DummyValue, "_config", _("Server Command"), - _("Copy the command to run ss-server on the server side")); - o.rawhtml = true; - o.cfgvalue = function(section_id) { - var server = uci.get('ss-redir', section_id, 'server'); - var password = uci.get('ss-redir', section_id, 'password'); - var encrypt = uci.get('ss-redir', section_id, 'encrypt'); - var kcp_port = uci.get('ss-redir', section_id, 'kcp_port') || 6441; - - var config = "ss-server -s " + server + " -p " + kcp_port + " -k " + password + " -m " + encrypt; - return ""; - } - // add a button to copy the command - o = s.taboption('server', form.Button, "_copy", _("Copy Command")); - o.inputstyle = 'apply'; - o.inputtitle = _("Copy the command to clipboard"); - o.onclick = function(ev) { - var textarea = document.querySelector('textarea'); - textarea.select(); - document.execCommand('copy'); - ui.addNotification(null, E('p', _('Copied to clipboard')), 'info'); - }; - - return m.render(); - } -}); diff --git a/luci-app-ss-redir/po/templates/ss-redir.pot b/luci-app-ss-redir/po/templates/ss-redir.pot deleted file mode 100644 index e69de29bb..000000000 diff --git a/luci-app-ss-redir/po/zh-cn b/luci-app-ss-redir/po/zh-cn deleted file mode 120000 index 8d69574dd..000000000 --- a/luci-app-ss-redir/po/zh-cn +++ /dev/null @@ -1 +0,0 @@ -zh_Hans \ No newline at end of file diff --git a/luci-app-ss-redir/po/zh_Hans/ss-redir.po b/luci-app-ss-redir/po/zh_Hans/ss-redir.po deleted file mode 100644 index 1d57c1fad..000000000 --- a/luci-app-ss-redir/po/zh_Hans/ss-redir.po +++ /dev/null @@ -1,51 +0,0 @@ -#: ss-redir package -msgid "ss-redir" -msgstr "SS转发服务" - -msgid "ss-redir redirect tcp service to kcptun client process." -msgstr "SS转发服务,将TCP流量重定向到本地KCP隧道,要结合本地KCP隧道服务一起使用。" - -msgid "Shadowsocks Server Settings" -msgstr "Shadowsocks服务器设置" - -msgid "ss-redir Settings" -msgstr "SS转发服务设置" - -msgid "The address of kcptun client listening on. default: 127.0.0.1" -msgstr "KCP隧道客户端监听地址,默认: 127.0.0.1" - -msgid "Server Address" -msgstr "服务器地址" - -msgid "The port of kcptun client listening on. default: 12948" -msgstr "KCP隧道客户端监听端口,默认: 12948" - -msgid "The password of shadowsocks server" -msgstr "Shadowsocks服务器密码" - -msgid "Server Port" -msgstr "服务器端口" - -msgid "Encryption Method" -msgstr "加密方式" - -msgid "The encryption method of shadowsocks server" -msgstr "Shadowsocks服务器的加密方式" - -msgid "Enable ss-redir service" -msgstr "启用SS转发服务" - -msgid "Server Command" -msgstr "服务器端命令" - -msgid "Copy the command to run ss-server on the server side" -msgstr "复制在服务器端运行ss-server的命令" - -msgid "Copy Command" -msgstr "复制命令" - -msgid "Compy the command to clipboard" -msgstr "复制服务器端命令到剪贴板" - -msgid "Copied to clipboard" -msgstr "已复制到剪贴板" \ No newline at end of file diff --git a/luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json b/luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json deleted file mode 100644 index a4c12b893..000000000 --- a/luci-app-ss-redir/root/usr/share/luci/menu.d/luci-app-ss-redir.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin/vpn/ss-redir": { - "title": "ss-redir", - "action": { - "type": "view", - "path": "ss-redir" - }, - "depends": { - "uci": { "ss-redir": true } - } - } -} \ No newline at end of file diff --git a/luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json b/luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json deleted file mode 100644 index 8ec797424..000000000 --- a/luci-app-ss-redir/root/usr/share/rpcd/acl.d/luci-app-ss-redir.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "luci-app-ss-redir": { - "description": "Grant access to LuCI app ss-redir", - "read": { - "ubus": { - "service": [ "list" ] - }, - "uci": ["ss-redir"] - }, - "write": { - "uci": ["ss-redir"] - } - } -} diff --git a/luci-app-yt-dlp/Makefile b/luci-app-yt-dlp/Makefile deleted file mode 100644 index e190d9a36..000000000 --- a/luci-app-yt-dlp/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -# This is free software, licensed under the Apache License, Version 2.0 - -include $(TOPDIR)/rules.mk - -LUCI_TITLE:=LuCI Support for yt-dlp -LUCI_DEPENDS:=+yt-dlp - -PKG_LICENSE:=Apache-2.0 -PKG_MAINTAINER:=Dengfeng Liu - -include $(TOPDIR)/feeds/luci/luci.mk - -# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-yt-dlp/README.md b/luci-app-yt-dlp/README.md deleted file mode 100644 index 0d98e1362..000000000 --- a/luci-app-yt-dlp/README.md +++ /dev/null @@ -1,42 +0,0 @@ -## luci-app-yt-dlp -该项目是yt-dlp的luci界面,通过该界面可以下载youtube、Twitter、B站等yt-dlp支持下载的视频网站。 - -### 视频演示 - -
- - luci-app-yt-dlp_intro_video - -
- - -## 如何集成到openwrt中编译 - -1. 复制仓库中的文件到如下目录,并执行安装 - -``` -feeds/luci/applications/luci-app-yt-dlp/ -./scripts/feeds install luci -a -``` - -2. 选择路径 - -`make menuconfig` - -LuCI > 3. Applications > luci-app-yt-dlp - -3. 编译openwrt固件 - -``` -make -j4 -``` - -4. 单独编译 - -``` -make package/luci-app-yt-dlp/compile -``` - -## 联系方式 - -QQ群:331230369 diff --git a/luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js b/luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js deleted file mode 100644 index f1b88c035..000000000 --- a/luci-app-yt-dlp/htdocs/luci-static/resources/view/yt-dlp.js +++ /dev/null @@ -1,171 +0,0 @@ -'use strict'; -'require view'; -'require dom'; -'require fs'; -'require ui'; -'require uci'; -'require network'; -'require rpc'; -'require tools.github as github'; - -return view.extend({ - load: function() { - return L.resolveDefault(fs.list('/tmp/yt-dlp/'), null); - }, - - updateTable: function(table, files) { - var rows = []; - - if (files == null) - return; - - for(var i = 0; i < files.length; i++) { - var file = files[i]; - - rows.push([ - file.name, - file.size, - E('span', { 'class': 'diag-action' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action important', - 'click': ui.createHandlerFn(this, 'handleDownloadVideo', file.name) - }, _('Download')), - E('button', { - 'class': 'cbi-button cbi-button-action cbi-button-negative', - 'click': ui.createHandlerFn(this, 'handleDeleteVideo', file.name) - }, _('Delete')) - ]), - ]); - } - - cbi_update_table(table, rows, E('em', _('No information available'))); - }, - - handleYtdlp: function(ev) { - var exec = '/usr/bin/yt-dlp', - addr = ev.currentTarget.parentNode.previousSibling.value, - args = ['-o', "/tmp/yt-dlp/%(title)s.%(ext)s", addr]; - - if (addr == '') { - ui.addNotification(null, E('p', _('Please enter a valid URL'))); - return; - } - - var buttons = document.querySelectorAll('.diag-action > .cbi-button'); - - for (var i = 0; i < buttons.length; i++) - buttons[i].setAttribute('disabled', 'true'); - - return fs.exec(exec, args).then(L.bind(function(res) { - var out = document.querySelector('.command-output'); - out.style.display = ''; - - dom.content(out, [ res.stdout || '', res.stderr || '' ]); - if (res.stdout) { - L.resolveDefault(fs.list('/tmp/yt-dlp/'), null).then(L.bind(function(files) { - var table = document.getElementById('videos'); - this.updateTable('#videos', files); - }, this)); - } - }, this)).catch(function(err) { - ui.addNotification(null, E('p', [ err ])); - return null - }).finally(function() { - for (var i = 0; i < buttons.length; i++) - buttons[i].removeAttribute('disabled'); - }); - }, - - handleDeleteVideo: function(filename, ev) { - var file = '/tmp/yt-dlp/' + filename; - return fs.remove(file).then(L.bind(function(res) { - L.resolveDefault(fs.list('/tmp/yt-dlp/'), null).then(L.bind(function(files) { - var table = document.getElementById('videos'); - this.updateTable("#videos", files); - }, this)); - }, this)).catch(function(err) { - ui.addNotification(null, E('p', [ err ])); - return null - }); - }, - - - handleDownloadVideo: function(filename, ev) { - var file = '/tmp/yt-dlp/' + filename; - var form = E('form', { - 'method': 'post', - 'action': L.env.cgi_base + '/cgi-download', - 'enctype': 'application/x-www-form-urlencoded' - }, [ - E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }), - E('input', { 'type': 'hidden', 'name': 'path', 'value': file }), - E('input', { 'type': 'hidden', 'name': 'filename', 'value': filename }) - ]); - - ev.currentTarget.parentNode.appendChild(form); - - form.submit(); - form.parentNode.removeChild(form); - }, - - clearInput: function(ev) { - document.getElementById('video_url').value = ''; - }, - - render: function(files) { - var video_url; - var tbl_videos = E('table', { 'class': 'table', 'id': 'videos' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th', 'style': 'width:60%' }, _('Name')), - E('th', { 'class': 'th', 'style': 'width;20%' }, _('Size')), - E('th', { 'class': 'th', 'style': 'width:20%' }, _('Action')), - ]) - ]); - var description = github.luci_desc('Download video from Youtube, Facebook, Twitter, etc.', 'liudf0716', 'yt-dlp') - - var v = E([], [ - E('h2', {}, [ _('Download Video') ]), - E('div', { 'class': 'cbi-section-descr' }, description ), - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td left' }, [ - E('input', { - 'style': 'margin:5px 0; width: 50% !important;', - 'type': 'text', - 'id': 'video_url', - 'value': video_url, - 'placeholder': _('Enter video URL') - }), - E('span', { 'class': 'diag-action' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': ui.createHandlerFn(this, 'handleYtdlp') - }, [ _('Yt-dlp Online') ]) - ]), - E('span', { 'class': 'diag-action' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action cbi-button-negative', - 'click': ui.createHandlerFn(this, 'clearInput') - }, [ _('Clear Input') ]) - ]),] - ), - ]) - ]), - E('pre', { 'class': 'command-output', 'style': 'display:none' }), - E('div', { 'class': 'cbi-section' }, [ - E('div', { 'class': 'left' }, [ - E('h3', _('Download Video Details')), - tbl_videos - ]) - ]) - ]); - - this.updateTable(tbl_videos, files); - - return v; - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/luci-app-yt-dlp/luci-app-yt-dlp_intro.png b/luci-app-yt-dlp/luci-app-yt-dlp_intro.png deleted file mode 100644 index 76a877143..000000000 Binary files a/luci-app-yt-dlp/luci-app-yt-dlp_intro.png and /dev/null differ diff --git a/luci-app-yt-dlp/po/zh-cn b/luci-app-yt-dlp/po/zh-cn deleted file mode 120000 index 8d69574dd..000000000 --- a/luci-app-yt-dlp/po/zh-cn +++ /dev/null @@ -1 +0,0 @@ -zh_Hans \ No newline at end of file diff --git a/luci-app-yt-dlp/po/zh_Hans/yt-dlp.po b/luci-app-yt-dlp/po/zh_Hans/yt-dlp.po deleted file mode 100644 index 22ff3225b..000000000 --- a/luci-app-yt-dlp/po/zh_Hans/yt-dlp.po +++ /dev/null @@ -1,29 +0,0 @@ -msgid "Download Video" -msgstr "短视频下载" - -msgid "Download Video Details" -msgstr "下载视频详情" - -msgid "Download video from Youtube, Facebook, Twitter, etc." -msgstr "从各大网站下载视频,支持B站,Youtube, Twitter等" - -msgid "Yt-dlp Online" -msgstr "下载视频" - -msgid "Please enter a valid URL" -msgstr "请输入有效的URL" - -msgid "yt-dlp" -msgstr "短视频下载" - -msgid "Action" -msgstr "操作" - -msgid "Download" -msgstr "下载到本地" - -msgid "Clear Input" -msgstr "清空输入" - -msgid "Enter video URL" -msgstr "输入视频URL" \ No newline at end of file diff --git a/luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp b/luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp deleted file mode 100755 index ee1f3b4eb..000000000 --- a/luci-app-yt-dlp/root/usr/libexec/rpcd/yt-dlp +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -ytdlp() { - url=$1 - output=$2 - /usr/bin/yt-dlp $url -o $output -} - -handleRequest() { - action=$1 - - if [ "$action" = "list" ]; then - echo "{\"download\":{\"description\":\"Download video\",\"params\":{\"url\":{\"type\":\"string\",\"description\":\"Video URL\"}}}}" - elif [ "$action" = "call" ]; then - method=$2 - - if [ "$method" = "download" ]; then - output="/tmp/yt-dlp-output.mp4" - read url - ytdlp "$url" "$output" - result="{\"result\":\"ok\",\"data\":{\"url\":\"$url\",\"output\":\"$output\"}}" - echo "$result" - else - errorResult="{\"result\":\"error\",\"message\":\"Unknown method\"}" - echo "$errorResult" - fi - fi -} - -handleRequest "$@" diff --git a/luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json b/luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json deleted file mode 100644 index 2997954ea..000000000 --- a/luci-app-yt-dlp/root/usr/share/luci/menu.d/luci-app-yt-dlp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "admin/services/yt-dlp": { - "title": "yt-dlp", - "action": { - "type": "view", - "path": "yt-dlp" - }, - "depends": { - "acl": [ "luci-app-yt-dlp" ] - } - } -} diff --git a/luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json b/luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json deleted file mode 100644 index 7a42309e7..000000000 --- a/luci-app-yt-dlp/root/usr/share/rpcd/acl.d/luci-app-yt-dlp.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "luci-app-yt-dlp": { - "description": "Grant access to LuCI app yt-dlp", - "read": { - "ubus": { - "file": [ "exec", "read", "stat" ], - "luci": [ "isFfmpegOrWhispercppRunning" ] - }, - "file": { - "/tmp/yt-dlp/*": [ "read" ] - }, - "cgi-io": [ "download" ] - }, - "write": { - "cgi-io": [ "upload"], - "file": { - "/usr/bin/yt-dlp": ["exec", "write" ], - "/tmp/yt-dlp/*": [ "write", "remove" ] - }, - "ubus": { - "file": [ "exec", "remove", "write" ], - "yt-dlp": [ "download" ] - } - } - } -} diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile index 24f086470..e822d80b5 100644 --- a/luci-theme-footstrap/Makefile +++ b/luci-theme-footstrap/Makefile @@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap FOOTSTRAP_VERSION?= ifneq ($(FOOTSTRAP_VERSION),) PKG_VERSION:=$(FOOTSTRAP_VERSION) -PKG_RELEASE:=65 +PKG_RELEASE:=66 endif LUCI_TITLE:=Footstrap Theme diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js index ae11e3dc2..4ab69bf81 100644 --- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js +++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-fit.js @@ -885,13 +885,44 @@ function lateDrift(ref, grow, floorShrink) { /* fresh distrust starts the recovery count at 0 too — a streak from a PREVIOUS * spell of distrust proves nothing about this one */ if (++_lateMisses >= LATE_MISS_LIMIT) { _engineTrusted = false; _lateHits = 0; } + /* MIRROR OF task refill2's WRITE-PATH FIX, ON THE NO-WRITE PATH — task nine. + * A miss here means the OFFSET did not fully move; it does not mean this tick's + * geometry is unknown. Leaving `_rest` as `run()`'s own mid-transition capture (the + * DOM already changed, nothing had compensated yet) makes THAT stale snapshot the + * `ref.at`/`was` the NEXT tick measures against, same as the write path used to + * before `rememberRest(true)` was added there. `seen` and `el`'s rect, just read, + * ARE the true current position — uncorrected, but real — so the next comparison + * should start from here, not from before this tick began. Measured, `../tmp/ + * task-nine/`: without this, `firefox owrt2410 @390 top overview` counted a SECOND + * phantom miss off the stale baseline and tripped `_engineTrusted` false while + * REPEAT's own mark never moved (misses [true,true,false]) — e0b6db4's fault on the + * other side of the same comparison. */ + rememberRest(true); return; } /* else: within table-row rounding — drift is still whatever it was (< 1 per the guard * above), so the plain `drift < 1` return two lines down is what fires, unwritten and * uncounted: the engine did the job. */ } - if (Math.abs(drift) < 1) return; /* the engine put it back */ + if (Math.abs(drift) < 1) { + /* SAME MIRROR AS ABOVE, FOR THE "engine already got it right" EXIT — task nine. + * Gated on `grow` OR `floorShrink`, not unconditional: a tick where this floored box + * neither grew nor shrank pays nothing extra here (P5), and `run()`'s own synchronous + * reference is only ever wrong relative to what THIS tick's mutation did. `grow` alone + * is not enough — REPEAT's own SHRINK leg (the pad it removes between refills) is a + * real box change `grow` reads as <=1 (growth witness is deliberately one-sided, + * task detector), so gating on `grow > 1` alone left the SAME hole one level down: the + * shrink between refill 1 and refill 2 left `_rest` at run()'s mid-transition capture, + * and refill 2 read ITS drift against that stale baseline. Where it did change (either + * way) and the engine handled it without a write, `run()`'s snapshot is still what the + * NEXT tick's `lateDrift()` would measure against — masking a real residual as "small" + * until it surfaces as a flat, un-recovered offset. Measured, `../tmp/task-nine/`: + * gating on `grow > 1` alone left `/admin/network/dhcp @390` at 47-59px off on the + * second of three back-to-back refills, unchanged — chromium/firefox/webkit alike, + * `_engineTrusted` true throughout. */ + if (grow > 1 || floorShrink > 1) rememberRest(true); + return; /* the engine put it back */ + } if (Math.abs(drift) > (window.innerHeight || 800)) return; const sc = scroller(); const at = sc ? sc.scrollTop : window.scrollY; diff --git a/ss-redir/Makefile b/ss-redir/Makefile deleted file mode 100644 index 578216a0e..000000000 --- a/ss-redir/Makefile +++ /dev/null @@ -1,50 +0,0 @@ -# -# Copyright (C) 2022 Dengfeng Liu -# -# This is free software, licensed under the GNU General Public License v3. -# See /LICENSE for more information. -# - -include $(TOPDIR)/rules.mk - -PKG_NAME:=ss-redir -PKG_VERSION:=0.1.0 -PKG_RELEASE:=1 - -PKG_MAINTAINER:=Dengfeng Liu -PKG_LICENSE:=GPL-3.0-or-later -PKG_LICENSE_FILES:=COPYING - -include $(INCLUDE_DIR)/package.mk - -define Package/ss-redir - SUBMENU:=Web Servers/Proxies - SECTION:=net - CATEGORY:=Network - TITLE:= simplify ss-redir usage - DEPENDS:=+shadowsocks-libev-ss-redir -endef - -define Package/ss-redir/description - simplify ss-redir usage -endef - -define Build/Compile -endef - -define Package/ss-redir/conffiles - /etc/config/ss-redir -endef - -define Package/ss-redir/install - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_DIR) $(1)/etc/uci-defaults - $(INSTALL_DIR) $(1)/usr/share/ss-redir - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_BIN) ./files/ss-redir.init $(1)/etc/init.d/ss-redir - $(INSTALL_BIN) ./files/ss-redir.conf $(1)/etc/config/ss-redir - $(INSTALL_DATA) ./files/firewall4.include $(1)/usr/share/ss-redir/firewall.include - $(INSTALL_BIN) ./files/ss-redir.defaults.nftables $(1)/etc/uci-defaults/99-ss-redir -endef - -$(eval $(call BuildPackage,ss-redir)) diff --git a/ss-redir/files/firewall4.include b/ss-redir/files/firewall4.include deleted file mode 100644 index 916b43913..000000000 --- a/ss-redir/files/firewall4.include +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -/etc/init.d/ss-redir enabled && /etc/init.d/ss-redir restart -exit 0 diff --git a/ss-redir/files/ss-redir.conf b/ss-redir/files/ss-redir.conf deleted file mode 100644 index 716f801dd..000000000 --- a/ss-redir/files/ss-redir.conf +++ /dev/null @@ -1,6 +0,0 @@ -config ss_redir 'server' - option enabled '0' - option server '127.0.0.1' - option server_port '12948' - option password 'password' - option encrypt 'aes-256-cfb' diff --git a/ss-redir/files/ss-redir.defaults.nftables b/ss-redir/files/ss-redir.defaults.nftables deleted file mode 100644 index ff6ab60f8..000000000 --- a/ss-redir/files/ss-redir.defaults.nftables +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -uci -q batch <<-EOT - delete firewall.ss_redir - set firewall.ss_redir=include - set firewall.ss_redir.type=script - set firewall.ss_redir.path=/usr/share/ss-redir/firewall.include - commit firewall -EOT - -exit 0 diff --git a/ss-redir/files/ss-redir.init b/ss-redir/files/ss-redir.init deleted file mode 100644 index 8c7487550..000000000 --- a/ss-redir/files/ss-redir.init +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/sh /etc/rc.common -# -# Copyright (C) 2023 Dengfeng Liu -# -# This is free software, licensed under the GNU General Public License v3. -# See /LICENSE for more information. -# - -USE_PROCD=1 -START=99 - -CONF="ss-redir" -PROG="/usr/bin/ss-redir" -GO_SS2="/usr/bin/go-ss2" -CONFILE="/tmp/ss-redir.json" - -mk_ss_redir_conf() { - local stream_methods='"table", "rc4", "rc4-md5", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", "salsa20", "chacha20", "chacha20-ietf"' - local aead_methods='"aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305"' - - uci_validate_section ss-redir ss_redir "server" \ - 'server:host:127.0.0.1' \ - 'server_port:port:12948' \ - 'password:string' \ - "encrypt:or($stream_methods, $aead_methods)" \ - 'local_address:host:0.0.0.0' \ - 'local_port:port:1088' \ - 'fast_open:bool:1' \ - 'ipv6_first:bool:0' \ - 'no_delay:bool:0' \ - 'reuse_port:bool:1' \ - 'mode:or("tcp_only", "udp_only", "tcp_and_udp"):tcp_only' \ - 'mtu:uinteger:1350' \ - 'timeout:uinteger:60' - - json_init - json_add_string server $server - json_add_int server_port $server_port - json_add_string method $encrypt - json_add_string password $password - json_add_string local_address $local_address - json_add_int local_port $local_port - json_add_string mode $mode - json_add_int mtu $mtu - json_add_int timeout $timeout - json_add_boolean fast_open $fast_open - json_add_boolean ipv6_first $ipv6_first - json_add_boolean no_delay $no_delay - json_add_boolean reuse_port $reuse_port - json_dump -i >"$CONFILE" -} - -add_ss_redir_rule() { - nft add set inet fw4 ss-redir_set { type ipv4_addr\; } - nft add set inet fw4 ss-redir_set6 { type ipv6_addr\; } - nft add set inet fw4 all_proxy_set { type ipv4_addr\; } - nft add set inet fw4 all_proxy_set6 { type ipv6_addr\; } - nft add chain inet fw4 ss-redir_chain - nft add rule inet fw4 dstnat iifname br-lan jump ss-redir_chain - nft add rule inet fw4 ss-redir_chain ip saddr @all_proxy_set meta l4proto tcp redirect to 1080 - nft add rule inet fw4 ss-redir_chain ip6 saddr @all_proxy_set6 meta l4proto tcp redirect to 1080 - nft add rule inet fw4 ss-redir_chain ip daddr @ss-redir_set meta l4proto tcp redirect to 1088 - nft add rule inet fw4 ss-redir_chain ip daddr @ss-redir_set meta l4proto udp redirect to 1088 - nft add rule inet fw4 ss-redir_chain ip6 daddr @ss-redir_set6 meta l4proto udp redirect to 1088 - nft add rule inet fw4 ss-redir_chain ip6 daddr @ss-redir_set6 meta l4proto tcp redirect to 1088 - -} - -clear_ss_redir_rule() { - if [ -z "$(nft -a list chain inet fw4 dstnat | grep ss-redir_chain)" ]; then - echo "no ss-redir rule" - return - fi - # get the handle of the ss-redir_chain in dstnat - handle_num=$(nft -a list chain inet fw4 dstnat | grep ss-redir_chain | awk '{print $7}') - if [ -z "$handle_num" ]; then - echo "cant get the handle of ss-redir_chain in dstnat" - return - fi - nft delete rule inet fw4 dstnat handle $handle_num - nft flush chain inet fw4 ss-redir_chain - nft delete chain inet fw4 ss-redir_chain - nft delete set inet fw4 ss-redir_set - nft delete set inet fw4 ss-redir_set6 - nft delete set inet fw4 all_proxy_set - nft delete set inet fw4 all_proxy_set6 -} - -ss_redir_rule() { - clear_ss_redir_rule - add_ss_redir_rule -} - -get_proxy_command() { - if [ -x "$GO_SS2" ]; then - echo "$GO_SS2 -c 'ss://$encrypt:$password@$server:$server_port' -redir :$local_port" - else - echo "$PROG -c $CONFILE" - fi -} - -start_service() { - config_load "$CONF" - local enabled - - config_get_bool enabled "server" "enabled" "0" - [ "$enabled" -eq "1" ] || { - echo "service does not enabled" - clear_ss_redir_rule - return 1 - } - - mk_ss_redir_conf - [ ! -f $CONFILE ] && return 1 - - - procd_open_instance - if [ -x "$GO_SS2" ]; then - procd_set_param command "$GO_SS2" -c "ss://$encrypt:$password@$server:$server_port" -verbose -redir ":$local_port" - procd_set_param stdout 1 - procd_set_param stderr 1 - else - procd_set_param command "$PROG" -c "$CONFILE" - fi - procd_set_param respawn - procd_close_instance - - ss_redir_rule -} - -reload_service() { - stop - start -} - -service_triggers() { - procd_add_reload_trigger "$CONF" -} \ No newline at end of file diff --git a/torrserver/Makefile b/torrserver/Makefile index dc887fabe..665fa777a 100644 --- a/torrserver/Makefile +++ b/torrserver/Makefile @@ -8,12 +8,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=torrserver -PKG_VERSION:=MatriX.144.1 -PKG_RELEASE:=19 +PKG_VERSION:=MatriX.144 +PKG_RELEASE:=20 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git -PKG_SOURCE_VERSION:=309a5917c12764c82791b555fb61a60f8e2fee44 +PKG_SOURCE_VERSION:=c416a8f2b89b244c779f03f1f86e2f0f690c2668 PKG_MIRROR_HASH:=skip PKG_MAINTAINER:=Konstantine Shevlakov