mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-07 04:34:05 +08:00
🏅 Sync 2026-11-07 00:22:58
This commit is contained in:
1 parent
47b676665c
commit
0810753c5c
50 files changed
+1588
-1305
No files matched your search
@@ -8,7 +8,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=apfree-wifidog
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/liudf0716/apfree-wifidog.git
|
||||
|
||||
@@ -19,6 +19,7 @@ handle_gateway() {
|
||||
config_get gateway_channel "$section" gateway_channel
|
||||
config_get gateway_id "$section" gateway_id
|
||||
config_get gateway_auth_enabled "$section" gateway_auth_enabled 1
|
||||
config_get gateway_subnetv4 "$section" gateway_subnetv4
|
||||
|
||||
|
||||
if [ -z "$gateway_name" ] || [ -z "$gateway_channel" ] ; then
|
||||
@@ -36,23 +37,26 @@ handle_gateway() {
|
||||
uci set wifidogx."$section".gateway_id="$gateway_id"
|
||||
uci commit wifidogx
|
||||
fi
|
||||
# according to the gateway_name to get the subnetv4
|
||||
local gateway_ipv4=$(ifconfig "$gateway_name" | awk '/inet addr:/ {print $2}' | cut -d: -f2)
|
||||
local gateway_maskv4=$(ifconfig "$gateway_name" | awk '/Mask:/ {print $4}' | cut -d: -f2)
|
||||
[ -z "$gateway_ipv4" ] && echo "Failed to get gateway_ipv4 for $gateway_name" >&2 && return
|
||||
[ -z "$gateway_maskv4" ] && echo "Failed to get gateway_maskv4 for $gateway_name" >&2 && return
|
||||
|
||||
# change the gateway_ip4/gateway_maskv4 to CIDR format
|
||||
local mask_bits=0
|
||||
local mask_value=$(printf '%d' "0x$(echo $gateway_maskv4 | tr '.' ' ' | awk '{printf "%02x%02x%02x%02x",$1,$2,$3,$4}')")
|
||||
while [ $mask_value -ne 0 ]; do
|
||||
mask_bits=$((mask_bits + (mask_value & 1)))
|
||||
mask_value=$((mask_value >> 1))
|
||||
done
|
||||
gateway_subnetv4="$gateway_ipv4/$mask_bits"
|
||||
|
||||
uci set wifidogx."$section".gateway_subnetv4="$gateway_subnetv4"
|
||||
uci commit wifidogx
|
||||
# If gateway_subnetv4 is not configured in UCI, detect it from interface
|
||||
if [ -z "$gateway_subnetv4" ]; then
|
||||
local gateway_ipv4=$(ifconfig "$gateway_name" | awk '/inet addr:/ {print $2}' | cut -d: -f2)
|
||||
local gateway_maskv4=$(ifconfig "$gateway_name" | awk '/Mask:/ {print $4}' | cut -d: -f2)
|
||||
[ -z "$gateway_ipv4" ] && echo "Failed to get gateway_ipv4 for $gateway_name" >&2 && return
|
||||
[ -z "$gateway_maskv4" ] && echo "Failed to get gateway_maskv4 for $gateway_name" >&2 && return
|
||||
|
||||
# change the gateway_ip4/gateway_maskv4 to CIDR format
|
||||
local mask_bits=0
|
||||
local mask_value=$(printf '%d' "0x$(echo $gateway_maskv4 | tr '.' ' ' | awk '{printf "%02x%02x%02x%02x",$1,$2,$3,$4}')")
|
||||
while [ $mask_value -ne 0 ]; do
|
||||
mask_bits=$((mask_bits + (mask_value & 1)))
|
||||
mask_value=$((mask_value >> 1))
|
||||
done
|
||||
gateway_subnetv4="$gateway_ipv4/$mask_bits"
|
||||
|
||||
uci set wifidogx."$section".gateway_subnetv4="$gateway_subnetv4"
|
||||
uci commit wifidogx
|
||||
fi
|
||||
|
||||
printf "GatewaySetting {\n\tGatewayAuthEnabled %s\n\tGatewayInterface %s\n\tGatewayChannel %s\n\tGatewayID %s\n\tGatewaySubnetV4 %s\n}\n" \
|
||||
"${gateway_auth_enabled}" "${gateway_name}" "${gateway_channel}" "${gateway_id}" "${gateway_subnetv4}" >> "$CONFIGFILE"
|
||||
@@ -141,11 +145,16 @@ prepare_auth_server_settings() {
|
||||
fi
|
||||
|
||||
case "$auth_server_mode" in
|
||||
cloud|bypass)
|
||||
cloud)
|
||||
printf "AuthServerMode 0\n" >> "$CONFIGFILE"
|
||||
printf "DeviceID %s\nAuthServer {\n\tHostname %s\n\tHTTPPort %s\n\tPath %s\n}\n" \
|
||||
"$device_id" "$auth_server_hostname" "$auth_server_port" "$auth_server_path" >> "$CONFIGFILE"
|
||||
;;
|
||||
bypass)
|
||||
printf "AuthServerMode 1\n" >> "$CONFIGFILE"
|
||||
printf "DeviceID %s\nAuthServer {\n\tHostname %s\n\tHTTPPort %s\n\tPath %s\n}\n" \
|
||||
"$device_id" "$auth_server_hostname" "$auth_server_port" "$auth_server_path" >> "$CONFIGFILE"
|
||||
;;
|
||||
local)
|
||||
printf "AuthServerMode 2\n" >> "$CONFIGFILE"
|
||||
[ -n "$device_id" ] && printf "DeviceID %s\n" "$device_id" >> "$CONFIGFILE"
|
||||
@@ -155,6 +164,7 @@ prepare_auth_server_settings() {
|
||||
esac
|
||||
|
||||
[ -n "$internet_offline_file" ] && printf "InternetOfflineFile %s\n" "$internet_offline_file" >> "$CONFIGFILE"
|
||||
return 0
|
||||
}
|
||||
|
||||
prepare_longconn_settings() {
|
||||
@@ -266,11 +276,12 @@ prepare_wifidog_conf() {
|
||||
[ -n "$wildcard_white_list" ] && add_white_list_entries "wildcard_white_list" "wildcard_domain" "trusted_wildcard_domains"
|
||||
|
||||
prepare_external_interface
|
||||
prepare_auth_server_settings
|
||||
prepare_auth_server_settings || return 1
|
||||
prepare_longconn_settings
|
||||
prepare_device_info
|
||||
config_foreach handle_gateway gateway
|
||||
prepare_common_settings
|
||||
return 0
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
@@ -280,7 +291,7 @@ service_triggers() {
|
||||
start_service() {
|
||||
config_load "$NAME"
|
||||
|
||||
prepare_wifidog_conf
|
||||
prepare_wifidog_conf || return 1
|
||||
|
||||
if [ "$enabled" -eq 0 ]; then
|
||||
echo "wifidogx is disabled, exit..." >&2
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gately
|
||||
PKG_VERSION:=0.6.4.1
|
||||
PKG_RELEASE:=26
|
||||
PKG_VERSION:=0.6.5
|
||||
PKG_RELEASE:=27
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_KS_CLI:=$(ARCH)
|
||||
|
||||
PKG_NAME:=ks-cli
|
||||
PKG_VERSION:=0.8.8
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=0.8.9
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_SOURCE:=ks-cli-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kspeeder/docker_kspeeder/releases/download/v$(PKG_VERSION)/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI Support for apfree-wifidog
|
||||
LUCI_DEPENDS:=+apfree-wifidog
|
||||
LUCI_DEPENDS:=+apfree-wifidog +curl
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
|
||||
@@ -72,6 +72,30 @@ function renderStatus(isRunning) {
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
function validateWildcardDomain(section_id, value) {
|
||||
if (!value || value.length === 0)
|
||||
return true;
|
||||
if (/^(\*\.|\.)?[a-zA-Z0-9_]([a-zA-Z0-9-_]*\.)*[a-zA-Z0-9-_]+$/.test(value))
|
||||
return true;
|
||||
return _('Expecting a valid wildcard domain pattern like *.example.com or .example.com');
|
||||
}
|
||||
|
||||
function checkAndCleanGhostSection(config, type, name, excludeSid) {
|
||||
var sections = uci.sections(config, type);
|
||||
for (var i = 0; i < sections.length; i++) {
|
||||
var s = sections[i];
|
||||
var sname = s['.name'];
|
||||
if (sname === name && sname !== excludeSid) {
|
||||
if (s['.create'] != null) {
|
||||
uci.remove(config, sname);
|
||||
continue;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
render: function () {
|
||||
var m, s, o, ss;
|
||||
@@ -106,10 +130,11 @@ return view.extend({
|
||||
|
||||
// 添加一个下拉框,列出当前定义的认证服务列表,并选择当前选中的认证服务
|
||||
o = s.taboption('basic', form.ListValue, 'selected_auth_server', _('Authentication Server'),
|
||||
_('Select the authentication server to use for cloud authentication.'));
|
||||
_('Select the authentication server to use for cloud or bypass authentication.'));
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.depends('auth_server_mode', 'cloud');
|
||||
o.depends('auth_server_mode', 'bypass');
|
||||
o.load = function (section_id) {
|
||||
this.keylist = [];
|
||||
this.vallist = [];
|
||||
@@ -218,10 +243,10 @@ return view.extend({
|
||||
o.optional = true;
|
||||
|
||||
o = ss.option(form.Value, 'gateway_subnetv4', _('Gateway Subnetv4'),
|
||||
_('The ipv4 subnet of the gateway.'));
|
||||
_('The ipv4 subnet of the gateway. Leave empty to automatically detect from interface.'));
|
||||
o.datatype = 'cidr4';
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.placeholder = '192.168.80.0/24';
|
||||
|
||||
// advanced settings
|
||||
@@ -260,7 +285,7 @@ return view.extend({
|
||||
o.optional = false;
|
||||
o.default = 'chawrt@2026';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ttl_value', _('TTL Value'),
|
||||
o = s.taboption('advanced', form.Value, 'ttl_values', _('TTL Value'),
|
||||
_('The TTL value of the gateway support.'));
|
||||
o.datatype = 'string';
|
||||
o.rmempty = false;
|
||||
@@ -282,19 +307,13 @@ return view.extend({
|
||||
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();
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'longconn', 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();
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'auth', 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]);
|
||||
@@ -332,19 +351,13 @@ return view.extend({
|
||||
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;
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'longconn', newName, oldName)) {
|
||||
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;
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'auth', newName, 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);
|
||||
@@ -420,7 +433,7 @@ return view.extend({
|
||||
]);
|
||||
}
|
||||
|
||||
// Helper function to get MAC address with fallback
|
||||
// Helper function to get MAC address
|
||||
function getMacAddressPromise(results) {
|
||||
return callGetWanMac().then(function (response) {
|
||||
if (response && response.status === 'success') {
|
||||
@@ -434,40 +447,17 @@ 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...');
|
||||
return getMacAddressFallback(results);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function for MAC address fallback
|
||||
function getMacAddressFallback(results) {
|
||||
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;
|
||||
document.getElementById('mac-status').innerHTML = _('✓ WAN MAC address obtained (fallback): ') + macAddr;
|
||||
|
||||
results.deviceId = generateDeviceId(macAddr);
|
||||
document.getElementById('device-id-status').innerHTML = _('✓ Device ID generated: ') + results.deviceId;
|
||||
|
||||
results.locationId = generateLocationId();
|
||||
document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + results.locationId;
|
||||
} else {
|
||||
var errorMsg = (response && response.message) ? response.message : 'Unknown error';
|
||||
var errorMsg = (response && response.message) ? response.message : _('Unknown error');
|
||||
document.getElementById('mac-status').innerHTML = _('✗ Failed to get WAN MAC address: ') + errorMsg;
|
||||
document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID without MAC address');
|
||||
}
|
||||
}).catch(function (error) {
|
||||
document.getElementById('mac-status').innerHTML = _('✗ Error getting WAN MAC address: ') + error.message;
|
||||
document.getElementById('mac-status').innerHTML = _('✗ Error getting WAN MAC address: ') + (error.message || error);
|
||||
document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID due to MAC error');
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to get location with fallback
|
||||
// Helper function to get location
|
||||
function getLocationPromise(results) {
|
||||
return callGetLocation().then(function (response) {
|
||||
if (response && response.status === 'success') {
|
||||
@@ -477,66 +467,29 @@ return view.extend({
|
||||
formatAndSetCoordinates(results, lat, lon);
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + results.latitude + ', ' + results.longitude;
|
||||
} else {
|
||||
document.getElementById('location-status').innerHTML = _('RPC returned error, trying fallback...');
|
||||
return getLocationFallback(results);
|
||||
var errorMsg = (response && response.message) ? response.message : _('Location services unavailable');
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location data: ') + errorMsg;
|
||||
}
|
||||
}).catch(function (error) {
|
||||
document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...');
|
||||
return getLocationFallback(results);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function for location fallback
|
||||
function getLocationFallback(results) {
|
||||
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 = parseFloat(data.latitude).toFixed(6);
|
||||
lon = parseFloat(data.longitude).toFixed(6);
|
||||
} else if (data.loc) {
|
||||
var coords = data.loc.split(',');
|
||||
lat = parseFloat(coords[0]).toFixed(6);
|
||||
lon = parseFloat(coords[1]).toFixed(6);
|
||||
}
|
||||
|
||||
if (lat && lon) {
|
||||
formatAndSetCoordinates(results, lat, lon);
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained (fallback): ') + results.latitude + ', ' + results.longitude;
|
||||
} 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;
|
||||
document.getElementById('location-status').innerHTML = _('✗ Error getting location: ') + (error.message || error);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to format coordinates
|
||||
function formatAndSetCoordinates(results, lat, lon) {
|
||||
// Pad coordinates to required format
|
||||
if (lat >= 0) {
|
||||
lat = lat.padStart(10, '0');
|
||||
} else {
|
||||
lat = '-' + Math.abs(lat).toFixed(6).padStart(9, '0');
|
||||
function formatCoord(val) {
|
||||
var num = parseFloat(val);
|
||||
if (isNaN(num)) return '';
|
||||
var isNeg = num < 0;
|
||||
var absFixed = Math.abs(num).toFixed(6);
|
||||
var parts = absFixed.split('.');
|
||||
var intPart = parts[0].padStart(3, '0');
|
||||
var decPart = parts[1];
|
||||
return (isNeg ? '-' : '') + intPart + '.' + decPart;
|
||||
}
|
||||
|
||||
if (lon >= 0) {
|
||||
lon = lon.padStart(10, '0');
|
||||
} else {
|
||||
lon = '-' + Math.abs(lon).toFixed(6).padStart(9, '0');
|
||||
}
|
||||
|
||||
results.longitude = lon;
|
||||
results.latitude = lat;
|
||||
results.longitude = formatCoord(lon);
|
||||
results.latitude = formatCoord(lat);
|
||||
}
|
||||
|
||||
// Helper function to fill form fields
|
||||
@@ -790,19 +743,13 @@ return view.extend({
|
||||
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();
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'auth', 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();
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'longconn', 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]);
|
||||
@@ -840,19 +787,13 @@ return view.extend({
|
||||
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;
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'auth', newName, oldName)) {
|
||||
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;
|
||||
}
|
||||
if (checkAndCleanGhostSection('wifidogx', 'longconn', newName, 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);
|
||||
@@ -940,7 +881,7 @@ return view.extend({
|
||||
_('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';
|
||||
o.validate = validateWildcardDomain;
|
||||
o.placeholder = '.example.com';
|
||||
|
||||
o = s.taboption('rule', form.DynamicList, 'trusted_domains', _('Trusted Domains'),
|
||||
@@ -983,8 +924,9 @@ return view.extend({
|
||||
s.nodescriptions = true;
|
||||
|
||||
s.handleRemove = function (section_id, ev) {
|
||||
// according section_id to check whether it is used by app_white_list or mac_white_list
|
||||
var group = uci.get('wifidogx', section_id, 'g_type') === '1' ? 'app_white_list' : 'mac_white_list';
|
||||
// according section_id to check whether it is used by app_white_list, mac_white_list or wildcard_white_list
|
||||
var gtype = uci.get('wifidogx', section_id, 'g_type');
|
||||
var group = (gtype === '1') ? 'app_white_list' : (gtype === '3' ? 'wildcard_white_list' : 'mac_white_list');
|
||||
var groupList = uci.get('wifidogx', 'common', group);
|
||||
if (groupList) {
|
||||
for (var i = 0; i < groupList.length; i++) {
|
||||
@@ -1024,7 +966,7 @@ return view.extend({
|
||||
|
||||
o = s.option(form.DynamicList, 'wildcard_domain', _('Wildcard Domain'), _('The wildcard domain of the group.'));
|
||||
o.depends('g_type', '3');
|
||||
o.datatype = 'wildcard';
|
||||
o.validate = validateWildcardDomain;
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.placeholder = '.example.com';
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
http_fetch() {
|
||||
local url="$1"
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -s --connect-timeout 10 --max-time 30 "$url" 2>/dev/null
|
||||
elif command -v uclient-fetch >/dev/null 2>&1; then
|
||||
uclient-fetch -q -O - --timeout=10 "$url" 2>/dev/null
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q -O - -T 10 "$url" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
list)
|
||||
json_init
|
||||
@@ -79,7 +90,7 @@ case "$1" in
|
||||
# Try multiple location APIs for better reliability
|
||||
|
||||
# First try ipapi.co (HTTPS, more reliable)
|
||||
result=$(curl -s --connect-timeout 10 --max-time 30 "https://ipapi.co/json" 2>/dev/null)
|
||||
result=$(http_fetch "https://ipapi.co/json")
|
||||
|
||||
if [ $? -eq 0 ] && [ -n "$result" ]; then
|
||||
# Check if we got valid latitude/longitude
|
||||
@@ -99,7 +110,7 @@ case "$1" in
|
||||
fi
|
||||
|
||||
# If first API failed, try ipinfo.io as backup
|
||||
result=$(curl -s --connect-timeout 10 --max-time 30 "http://ipinfo.io/json" 2>/dev/null)
|
||||
result=$(http_fetch "http://ipinfo.io/json")
|
||||
|
||||
if [ $? -eq 0 ] && [ -n "$result" ]; then
|
||||
# Extract lat,lon from loc field (format: "lat,lon")
|
||||
|
||||
@@ -268,7 +268,7 @@ const preset_outbound = {
|
||||
['COMPATIBLE']
|
||||
],
|
||||
direct: [
|
||||
['', _('null')],
|
||||
['', _('none')],
|
||||
['DIRECT'],
|
||||
['GLOBAL']
|
||||
],
|
||||
@@ -711,16 +711,17 @@ const CBIGenValue = form.Value.extend({
|
||||
__name__: 'CBI.GenValue',
|
||||
|
||||
renderWidget(section_id, option_index, cfgvalue) {
|
||||
let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
if (!this.password)
|
||||
node.classList.add('control-group');
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, handleGenKey, section_id, this.hm_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
if (this.section.section) // modalonly
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, handleGenKey, section_id, this.et_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -730,13 +731,14 @@ const CBIGenText = CBITextValue.extend({
|
||||
__name__: 'CBI.GenText',
|
||||
|
||||
renderWidget(section_id, option_index, cfgvalue) {
|
||||
let node = CBITextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
|
||||
let node = CBITextValue.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
node.appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, handleGenKey, section_id, this.hm_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
if (this.section.section) // modalonly
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, handleGenKey, section_id, this.et_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -748,28 +750,29 @@ const CBICopyValue = form.Value.extend({
|
||||
readonly: true,
|
||||
|
||||
renderWidget(section_id, option_index, cfgvalue) {
|
||||
let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
node.classList.add('control-group');
|
||||
|
||||
node.appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
click: ui.createHandlerFn(this, async (section_id) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.formvalue(section_id));
|
||||
console.log('Content copied to clipboard!');
|
||||
} catch (e) {
|
||||
console.error('Failed to copy: ', e);
|
||||
}
|
||||
/* Deprecated
|
||||
let inputEl = document.getElementById(this.cbid(section_id)).querySelector('input');
|
||||
inputEl.select();
|
||||
document.execCommand("copy");
|
||||
inputEl.blur();
|
||||
*/
|
||||
return alert(_('Content copied to clipboard!'));
|
||||
}, section_id)
|
||||
}, [ _('Copy') ]));
|
||||
if (this.section.section) // modalonly
|
||||
node.appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
click: ui.createHandlerFn(this, async (section_id) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.formvalue(section_id));
|
||||
console.log('Content copied to clipboard!');
|
||||
} catch (e) {
|
||||
console.error('Failed to copy: ', e);
|
||||
}
|
||||
/* Deprecated
|
||||
let inputEl = document.getElementById(this.cbid(section_id)).querySelector('input');
|
||||
inputEl.select();
|
||||
document.execCommand("copy");
|
||||
inputEl.blur();
|
||||
*/
|
||||
return alert(_('Content copied to clipboard!'));
|
||||
}, section_id)
|
||||
}, [ _('Copy') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
click: ui.createHandlerFn(this, () => {
|
||||
this.hm_options.type = document.getElementById(cbid).value;
|
||||
|
||||
return hm.handleGenKey.call(this, this.hm_options);
|
||||
return hm.handleGenKey.call(this, section_id, this.hm_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
@@ -311,7 +311,7 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-positive',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, hm.handleGenKey, this.hm_options || this.option)
|
||||
click: ui.createHandlerFn(this, hm.handleGenKey, section_id, this.hm_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
@@ -616,10 +616,10 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
|
||||
o = s.taboption('field_general', hm.ListValue, 'rule', _('Sub rule'),
|
||||
_('Name of the Sub rule used for inbound matching.'));
|
||||
o.value.apply(o, ['', _('null')]);
|
||||
o.value.apply(o, ['', _('none')]);
|
||||
o.load = function(section_id) {
|
||||
return hm.loadLabel.call(this, [
|
||||
['', _('null')],
|
||||
['', _('none')],
|
||||
...hm.loadLabelValues(this.config, 'subrule-group')
|
||||
], section_id);
|
||||
}
|
||||
@@ -960,7 +960,7 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
click: ui.createHandlerFn(this, () => {
|
||||
this.hm_options.type = document.getElementById(cbid).value;
|
||||
|
||||
return hm.handleGenKey.call(this, this.hm_options);
|
||||
return hm.handleGenKey.call(this, section_id, this.hm_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
@@ -1213,7 +1213,7 @@ function renderListeners(s, uciconfig, isClient) {
|
||||
click: ui.createHandlerFn(this, () => {
|
||||
this.hm_options.params = document.getElementById(cbid).value;
|
||||
|
||||
return hm.handleGenKey.call(this, this.hm_options);
|
||||
return hm.handleGenKey.call(this, section_id, this.hm_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
|
||||
@@ -611,7 +611,7 @@ return view.extend({
|
||||
click: ui.createHandlerFn(this, () => {
|
||||
this.hm_options.params = document.getElementById(cbid).value;
|
||||
|
||||
return hm.handleGenKey.call(this, this.hm_options);
|
||||
return hm.handleGenKey.call(this, section_id, this.hm_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
|
||||
@@ -1071,16 +1071,19 @@ return view.extend({
|
||||
so.depends('type', 'easytier');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_general', form.Flag, 'easytier_secure_mode', _('Secure Mode'));
|
||||
so = ss.taboption('field_general', form.Flag, 'easytier_secure_mode', _('Secure mode'));
|
||||
so.default = so.disabled;
|
||||
so.validate = function(section_id, value) {
|
||||
const secure_mode = this.getUIElement(section_id).node.querySelector('input');
|
||||
const peers = this.section.getOption('easytier_peers').formvalue(section_id);
|
||||
const local_private_key = this.section.getOption('easytier_local_private_key').formvalue(section_id);
|
||||
const local_public_key = this.section.getOption('easytier_local_public_key').formvalue(section_id);
|
||||
|
||||
// Force enabled
|
||||
if ((local_private_key && local_public_key) || peers.filter(e => new URL(e).searchParams.get('peer-public-key')).length > 0) {
|
||||
if (peers.filter(e => {
|
||||
try {
|
||||
return new URL(e).searchParams.get('peer-public-key');
|
||||
} catch {}
|
||||
}).length > 0
|
||||
) {
|
||||
secure_mode.checked = true;
|
||||
secure_mode.disabled = true;
|
||||
} else
|
||||
@@ -1104,12 +1107,13 @@ return view.extend({
|
||||
}
|
||||
so.password = true;
|
||||
so.validate = L.bind(hm.validateBase64Key, so, 44);
|
||||
so.depends('type', 'easytier');
|
||||
so.rmempty = false;
|
||||
so.depends('easytier_secure_mode', '1');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_general', hm.CopyValue, 'easytier_local_public_key', _('Client public key'),
|
||||
_('Base64 encoded X25519 public key.'));
|
||||
so.depends('type', 'easytier');
|
||||
so.depends('easytier_secure_mode', '1');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.taboption('field_general', form.DynamicList, 'easytier_exit_nodes', _('Exit nodes'),
|
||||
@@ -2464,7 +2468,7 @@ return view.extend({
|
||||
else
|
||||
this.hm_options.params = '';
|
||||
|
||||
return hm.handleGenKey.call(this, this.hm_options);
|
||||
return hm.handleGenKey.call(this, section_id, this.hm_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
|
||||
@@ -3247,7 +3247,7 @@ msgid "Secondary"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/fchomo/node.js:1074
|
||||
msgid "Secure Mode"
|
||||
msgid "Secure mode"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/fchomo/global.js:531
|
||||
|
||||
@@ -3282,7 +3282,7 @@ msgid "Secondary"
|
||||
msgstr "次要"
|
||||
|
||||
#: htdocs/luci-static/resources/view/fchomo/node.js:1074
|
||||
msgid "Secure Mode"
|
||||
msgid "Secure mode"
|
||||
msgstr "安全模式"
|
||||
|
||||
#: htdocs/luci-static/resources/view/fchomo/global.js:531
|
||||
|
||||
@@ -3282,7 +3282,7 @@ msgid "Secondary"
|
||||
msgstr "次要"
|
||||
|
||||
#: htdocs/luci-static/resources/view/fchomo/node.js:1074
|
||||
msgid "Secure Mode"
|
||||
msgid "Secure mode"
|
||||
msgstr "安全模式"
|
||||
|
||||
#: htdocs/luci-static/resources/view/fchomo/global.js:531
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.11.3
|
||||
PKG_RELEASE:=367
|
||||
PKG_RELEASE:=368
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -973,19 +973,6 @@ local current_node = map:get(section)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断密码是否经过url编码
|
||||
const isURLEncodedPassword = function(pwd) {
|
||||
if (!/%[0-9A-Fa-f]{2}/.test(pwd)) return false;
|
||||
try {
|
||||
const decoded = decodeURIComponent(pwd.replace(/\+/g, "%20"));
|
||||
const reencoded = encodeURIComponent(decoded);
|
||||
return reencoded === pwd;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
password = isURLEncodedPassword(password) ? decodeURIComponent(password) : password;
|
||||
|
||||
if (queryParam.plugin) {
|
||||
var pluginParams = decodeURIComponent(queryParam.plugin).split(";");
|
||||
plugin = pluginParams.shift();
|
||||
@@ -1775,13 +1762,10 @@ local current_node = map:get(section)
|
||||
var port = _parsedUrl.port;
|
||||
var search = _parsedUrl.search;
|
||||
var hash = _parsedUrl.hash;
|
||||
if (!username || !password) { //某些链接会把username和password之间的:进行编码
|
||||
const decoded = decodeURIComponent(username || password || "");
|
||||
const i = decoded.indexOf(":");
|
||||
if (i > -1) {
|
||||
username = decoded.slice(0, i);
|
||||
password = decoded.slice(i + 1);
|
||||
}
|
||||
// URL 已按明文冒号拆分;没有分隔符时整个 userinfo 是密码。
|
||||
if (link_split[1].split("@", 1)[0].indexOf(":") === -1) {
|
||||
password = username;
|
||||
username = "";
|
||||
}
|
||||
opt.set(dom_prefix + 'username', decodeURIComponent(username));
|
||||
opt.set(dom_prefix + 'password', decodeURIComponent(password));
|
||||
|
||||
@@ -876,20 +876,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
password = hostInfo[1] --一些链接用明文uuid做密码
|
||||
end
|
||||
|
||||
-- 判断密码是否经过url编码
|
||||
local function isURLEncodedPassword(pwd)
|
||||
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
|
||||
return false
|
||||
end
|
||||
local ok, decoded = pcall(UrlDecode, pwd)
|
||||
return ok and UrlEncode(decoded) == pwd
|
||||
end
|
||||
|
||||
local decoded = UrlDecode(password)
|
||||
if isURLEncodedPassword(password) and decoded then
|
||||
password = decoded
|
||||
end
|
||||
|
||||
local _method = (method or "none"):lower()
|
||||
method = (_method == "chacha20-poly1305" and "chacha20-ietf-poly1305") or
|
||||
(_method == "xchacha20-poly1305" and "xchacha20-ietf-poly1305") or _method
|
||||
@@ -1682,13 +1668,13 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
local contents = split(content, "@")
|
||||
local auth = contents[1] or ""
|
||||
local idx = auth:find(":", 1, true)
|
||||
if not idx then --修正某些链接会把username和password之间的:进行编码
|
||||
auth = UrlDecode(auth)
|
||||
idx = auth:find(":", 1, true)
|
||||
end
|
||||
-- 先按明文冒号拆分,再分别解码;没有分隔符时整个 userinfo 是密码。
|
||||
if idx then
|
||||
result.username = UrlDecode(auth:sub(1, idx - 1))
|
||||
result.password = UrlDecode(auth:sub(idx + 1))
|
||||
else
|
||||
result.username = ""
|
||||
result.password = UrlDecode(auth)
|
||||
end
|
||||
Info = (contents[2] or ""):gsub("/%?", "?")
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.11.3
|
||||
PKG_RELEASE:=169
|
||||
PKG_RELEASE:=170
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -961,19 +961,6 @@ local current_node = map:get(section)
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if the password is URL encoded
|
||||
const isURLEncodedPassword = function(pwd) {
|
||||
if (!/%[0-9A-Fa-f]{2}/.test(pwd)) return false;
|
||||
try {
|
||||
const decoded = decodeURIComponent(pwd.replace(/\+/g, "%20"));
|
||||
const reencoded = encodeURIComponent(decoded);
|
||||
return reencoded === pwd;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
password = isURLEncodedPassword(password) ? decodeURIComponent(password) : password;
|
||||
|
||||
if (queryParam.plugin) {
|
||||
var pluginParams = decodeURIComponent(queryParam.plugin).split(";");
|
||||
plugin = pluginParams.shift();
|
||||
@@ -1821,13 +1808,10 @@ local current_node = map:get(section)
|
||||
var port = _parsedUrl.port;
|
||||
var search = _parsedUrl.search;
|
||||
var hash = _parsedUrl.hash;
|
||||
if (!username || !password) { //某些链接会把username和password之间的:进行编码
|
||||
const decoded = decodeURIComponent(username || password || "");
|
||||
const i = decoded.indexOf(":");
|
||||
if (i > -1) {
|
||||
username = decoded.slice(0, i);
|
||||
password = decoded.slice(i + 1);
|
||||
}
|
||||
// URL splits on literal colons; without a separator, the entire userinfo is the password.
|
||||
if (link_split[1].split("@", 1)[0].indexOf(":") === -1) {
|
||||
password = username;
|
||||
username = "";
|
||||
}
|
||||
opt.set(dom_prefix + 'username', decodeURIComponent(username));
|
||||
opt.set(dom_prefix + 'password', decodeURIComponent(password));
|
||||
|
||||
@@ -1197,20 +1197,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
password = hostInfo[1] -- Some links use plaintext UUIDs as passwords.
|
||||
end
|
||||
|
||||
-- Determine if the password is URL encoded
|
||||
local function isURLEncodedPassword(pwd)
|
||||
if not pwd:find("%%[0-9A-Fa-f][0-9A-Fa-f]") then
|
||||
return false
|
||||
end
|
||||
local ok, decoded = pcall(UrlDecode, pwd)
|
||||
return ok and UrlEncode(decoded) == pwd
|
||||
end
|
||||
|
||||
local decoded = UrlDecode(password)
|
||||
if isURLEncodedPassword(password) and decoded then
|
||||
password = decoded
|
||||
end
|
||||
|
||||
local _method = (method or "none"):lower()
|
||||
method = (_method == "chacha20-poly1305" and "chacha20-ietf-poly1305") or
|
||||
(_method == "xchacha20-poly1305" and "xchacha20-ietf-poly1305") or _method
|
||||
@@ -2039,13 +2025,13 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
local contents = split(content, "@")
|
||||
local auth = contents[1] or ""
|
||||
local idx = auth:find(":", 1, true)
|
||||
if not idx then -- Fixed some links that encode the colon between username and password.
|
||||
auth = UrlDecode(auth)
|
||||
idx = auth:find(":", 1, true)
|
||||
end
|
||||
-- Split on the literal colon before decoding; without a separator, the entire userinfo is the password.
|
||||
if idx then
|
||||
result.username = UrlDecode(auth:sub(1, idx - 1))
|
||||
result.password = UrlDecode(auth:sub(idx + 1))
|
||||
else
|
||||
result.username = ""
|
||||
result.password = UrlDecode(auth)
|
||||
end
|
||||
Info = (contents[2] or ""):gsub("/%?", "?")
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=Web UI for QEMU Virtual Machine Simple (VMS)
|
||||
LUCI_DEPENDS:=@TARGET_x86_64 +qemu-vms +ttyd +usbutils +pciutils
|
||||
PKG_LICENSE:=GPLv3
|
||||
PKG_VERSION:=0.0.2
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ console_open() {
|
||||
return
|
||||
}
|
||||
|
||||
ttyd -p "$port" -o /usr/bin/vm-console "$vm" >/dev/null 2>&1 &
|
||||
ttyd -W -p "$port" -o /usr/bin/vm-console "$vm" >/dev/null 2>&1 &
|
||||
echo $! > "$pidfile"
|
||||
echo "$port" > "$portfile"
|
||||
|
||||
|
||||
@@ -1,9 +1,27 @@
|
||||
# 변경 기록
|
||||
|
||||
## [0.2.33-r1] - 2026-11-06
|
||||
|
||||
### 개선
|
||||
- 펌웨어 업데이트 후에 기존의 앱이 복원이 될 수 있도록 개선하였습니다.
|
||||
|
||||
|
||||
## [0.2.32-r2] - 2026-11-02
|
||||
|
||||
### 수정
|
||||
- 펌웨어를 업데이트한 뒤 앱 버전이 바뀌었을 때 기존 앱이 자동으로 다시 설치되지 않던 문제를 수정했습니다.
|
||||
- 앱을 복원할 때 공유기의 연결 추적 기능(conntrack)과 기존 시스템 패키지를 변경하지 않도록 보호했습니다.
|
||||
|
||||
### 개선
|
||||
- 이전에 설치했던 앱만 새 펌웨어에서 승인된 버전으로 안전하게 복원합니다.
|
||||
|
||||
### 테스트
|
||||
- 버전 변경 후 복원, 승인되지 않은 앱 차단, 기존 패키지 보호를 확인했습니다.
|
||||
|
||||
|
||||
## [0.2.32-r1] - 2026-11-02
|
||||
|
||||
### 추가
|
||||
|
||||
- Network Watch 기능을 출시하였습니다. 의심 징후를 탐지하고 감지하고 알립니다.
|
||||
|
||||
### 개선
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-smartsafehub
|
||||
PKG_VERSION:=0.2.32
|
||||
PKG_RELEASE:=12
|
||||
PKG_VERSION:=0.2.33
|
||||
PKG_RELEASE:=13
|
||||
|
||||
PKG_MAINTAINER:=Beomjun Kang <kals323@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "luci-app-smartsafehub-frontend",
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.33",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "luci-app-smartsafehub-frontend",
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.33",
|
||||
"dependencies": {
|
||||
"chart.js": "4.6.1",
|
||||
"preact": "11.1.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "luci-app-smartsafehub-frontend",
|
||||
"private": true,
|
||||
"version": "0.2.32",
|
||||
"version": "0.2.33",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24.0"
|
||||
|
||||
@@ -44,7 +44,7 @@ function installBootstrap(sessionId: string, host: HTMLElement): void {
|
||||
sessionId,
|
||||
rpcUrl: luciUrl('/admin/ubus'),
|
||||
assetBase: host.dataset.assetBase ?? '/luci-static/smartsafehub/',
|
||||
assetVersion: host.dataset.assetVersion ?? '0.2.32-r1',
|
||||
assetVersion: host.dataset.assetVersion ?? '0.2.33-r1',
|
||||
locale: document.documentElement.lang || 'ko',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ valid_sha() {
|
||||
}
|
||||
protected_package() {
|
||||
case "$1" in
|
||||
base-files|busybox|apk|procd|netifd|dnsmasq*|firewall*|odhcp*|dropbear|ubus*|rpcd*|uhttpd*|safeshield*|luci-app-smartsafehub|kernel|kmod-*) return 0;;
|
||||
base-files|busybox|apk|procd|netifd|conntrack|dnsmasq*|firewall*|odhcp*|dropbear|ubus*|rpcd*|uhttpd*|safeshield*|luci-app-smartsafehub|kernel|kmod-*) return 0;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
@@ -650,20 +650,45 @@ prepare_signed_index() {
|
||||
install_app() {
|
||||
preflight_install || return $?
|
||||
manifest="$(owned_manifest "$app")"
|
||||
# Existing app ownership must match the reviewed pins.
|
||||
if [ -e "$manifest" ]; then
|
||||
[ -s "$manifest" ] || return 28
|
||||
# Old sysupgrade ownership proves which package *names* Apps managed,
|
||||
# not which version/hash a newly published firmware has approved. A restore
|
||||
# may re-pin missing owned APKs only after validating the exact new catalog.
|
||||
# A user install and every already-installed APK remain strict: no adoption,
|
||||
# implicit upgrade, downgrade or replacement of existing system packages.
|
||||
owned_repin=0
|
||||
owned_approved="$stage/owned-approved"
|
||||
: > "$owned_approved"
|
||||
if [ -e "$manifest" ] || [ -L "$manifest" ]; then
|
||||
[ -f "$manifest" ] && [ ! -L "$manifest" ] && [ -s "$manifest" ] || return 28
|
||||
owned_count=0
|
||||
while IFS="$(printf '\t')" read -r name version hash; do
|
||||
valid_package "$name" && valid_version "$version" && valid_sha "$hash" || return 28
|
||||
protected_package "$name" && return 28
|
||||
shared_travelmate_dep "$name" && return 28
|
||||
# Duplicate or unapproved ownership must not authorize even a missing APK.
|
||||
awk -F '\t' -v name="$name" '$1 == name { found=1 } END { exit !found }' "$owned_approved" && return 28
|
||||
reviewed_pin="$(awk -F '\t' -v name="$name" '$1 == name {print; exit}' "$pins")"
|
||||
[ -n "$reviewed_pin" ] || return 28
|
||||
pin="$(printf '%s\t%s\t%s' "$name" "$version" "$hash")"
|
||||
grep -Fqx "$pin" "$pins" || return 28
|
||||
if [ "$pin" != "$reviewed_pin" ]; then
|
||||
[ "$source" = restore ] || return 28
|
||||
# An old installed version must never be silently upgraded or adopted.
|
||||
installed "$name" && return 28
|
||||
owned_repin=1
|
||||
fi
|
||||
printf '%s\n' "$reviewed_pin" >> "$owned_approved"
|
||||
owned_count=$((owned_count + 1)); [ "$owned_count" -le 32 ] || return 28
|
||||
done < "$manifest"
|
||||
# read skips an unterminated last line; reject truncated ownership instead
|
||||
# of treating it as permission for a different approved package set.
|
||||
[ "$owned_count" -gt 0 ] &&
|
||||
[ "$owned_count" -eq "$(awk 'END {print NR+0}' "$manifest")" ] || return 28
|
||||
fi
|
||||
need_install=0
|
||||
# Never adopt a package previously installed outside SmartSafeHub Apps.
|
||||
while IFS="$(printf '\t')" read -r name version hash; do
|
||||
pin="$(printf '%s\t%s\t%s' "$name" "$version" "$hash")"
|
||||
if [ -e "$manifest" ] && grep -Fqx "$pin" "$manifest"; then
|
||||
if [ -e "$manifest" ] && grep -Fqx "$pin" "$owned_approved"; then
|
||||
installed "$name" || need_install=1
|
||||
elif shared_travelmate_dep "$name" && installed "$name"; then
|
||||
# A system package was already present. Never adopt it or silently
|
||||
@@ -687,6 +712,11 @@ install_app() {
|
||||
prepare_signed_index || return $?
|
||||
"$APK" --repositories-file /dev/null -X "$local_index" add --simulate "$@" > "$stage/plan.log" 2>&1 || return 34
|
||||
review_install_plan "$stage/plan.log" "$base_names" || return 34
|
||||
# Only after index, archive hash and simulated install are validated may
|
||||
# missing packages be re-pinned. Keep the change atomic and retry-safe.
|
||||
if [ "$owned_repin" -eq 1 ]; then
|
||||
cp "$owned_approved" "$manifest.new" && mv -f "$manifest.new" "$manifest" || return 36
|
||||
fi
|
||||
# Write ownership immediately before modifying packages. If APK partially
|
||||
# succeeds, the UI offers repair/removal without adopting unrelated packages.
|
||||
while IFS="$(printf '\t')" read -r name version hash; do
|
||||
@@ -766,7 +796,7 @@ remove_app() {
|
||||
"$APK" del --simulate "$@" > "$stage/plan.log" 2>&1 || return 44
|
||||
# Removing an optional app must never implicitly remove system/network,
|
||||
# SafeShield or firmware kernel-module packages.
|
||||
if grep -Ei '(Purging|Removing|Upgrading|Downgrading) (base-files|busybox|apk|procd|netifd|dnsmasq[^ ]*|firewall[^ ]*|odhcp[^ ]*|dropbear|ubus[^ ]*|rpcd[^ ]*|uhttpd[^ ]*|safeshield[^ ]*|luci-app-smartsafehub|kernel|kmod-[^ ]*) ' "$stage/plan.log" >/dev/null; then
|
||||
if grep -Ei '(Purging|Removing|Upgrading|Downgrading) (base-files|busybox|apk|procd|netifd|conntrack|dnsmasq[^ ]*|firewall[^ ]*|odhcp[^ ]*|dropbear|ubus[^ ]*|rpcd[^ ]*|uhttpd[^ ]*|safeshield[^ ]*|luci-app-smartsafehub|kernel|kmod-[^ ]*) ' "$stage/plan.log" >/dev/null; then
|
||||
return 44
|
||||
fi
|
||||
review_remove_plan "$stage/plan.log" "$*" || return 44
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
<meta name="robots" content="noindex,nofollow,noarchive">
|
||||
<meta name="referrer" content="same-origin">
|
||||
<title>SmartSafeHub</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/luci-static/smartsafehub/favicon.svg?v=0.2.32-r1">
|
||||
<link rel="icon" type="image/svg+xml" href="/luci-static/smartsafehub/favicon.svg?v=0.2.33-r1">
|
||||
</head>
|
||||
<body style="margin:0;background:#f8fafc;min-height:100vh;">
|
||||
<div
|
||||
id="smartsafehub-entry-root"
|
||||
data-asset-base="/luci-static/smartsafehub/"
|
||||
data-asset-version="0.2.32-r1"
|
||||
data-asset-version="0.2.33-r1"
|
||||
style="display:block;width:100%;min-height:100vh;"
|
||||
></div>
|
||||
<noscript>SmartSafeHub를 사용하려면 JavaScript를 활성화해 주세요.</noscript>
|
||||
<script type="module" src="/luci-static/smartsafehub/app.js?v=0.2.32-r1"></script>
|
||||
<script type="module" src="/luci-static/smartsafehub/app.js?v=0.2.33-r1"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,3 @@
|
||||
var e,t,n,r,i,a,o,s,c,l,u=Symbol.for(`pv`),d={},f=[],p=/^m(i|n|o|s|text|space)$/,m=Array.isArray,h=f.slice,g=Object.assign;function _(e){e&&e.parentNode&&e.remove()}function v(e,t,n){var r,i,a,o={},s=arguments.length;for(a in t)a==`key`?r=t[a]:a==`ref`&&typeof e!=`function`?i=t[a]:o[a]=t[a];return s>2&&(o.children=s>3?h.call(arguments,2):n),y(e,o,r,i,null)}function y(n,r,i,a,o){var s={type:n,props:r,key:i,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:u,__v:o||++t,__i:-1,__u:0};return!o&&e.vnode&&e.vnode(s),s}function b(e){return e.children}function x(e,t){this.props=e,this.context=t,this.__g=0}function S(e,t){if(t==null)return e.__&&S(e.__,e.__i+1);for(var n;t<e.__k.length;t++)if((n=e.__k[t])&&n.__e)return n.__e;return typeof e.type!=`function`||e.props.__P?null:S(e)}function C(e){if((e=e.__)&&e.__c&&!e.props.__P)return e.__e=null,e.__k.some(function(t){return t&&(e.__e=t.__e)}),C(e)}function w(t){(8&t.__g||(t.__g|=8,n.push(t),i++))&&r==e.debounceRendering||((r=e.debounceRendering)||queueMicrotask)(T)}function T(){var t,o,s,c,l,d,f,p,m;try{for(o=1;n.length;)n.length>o&&n.sort(a),t=n.shift(),o=n.length,8&t.__g&&(c=void 0,l=void 0,d=(l=(s=t).__v).__e,f=[],p=[],(m=s.__P)&&((c=g({constructor:u},l)).__v=l.__v+1,e.vnode&&e.vnode(c),P(m,c,l,s.__n,m.namespaceURI,32&l.__u?[d]:null,f,d||S(l),32&l.__u,p),c.__v=l.__v,c.__.__k[c.__i]=c,ee(f,c,p),l.__=l.__e=null,c.__e!=d&&C(c)))}finally{(i=n.length)&&(r||queueMicrotask)(T)}}function E(e,t,n,r,i,a,o,s,c,l,u){var p,m,h,g,_,v,y=r.__k||f,b=t.length;for(c=D(n,t,y,c,b),p=0;p<b;p++)(h=n.__k[p])!=null&&(m=~h.__i&&y[h.__i]||d,h.__i=p,v=P(e,h,m,i,a,o,s,c,l,u),g=h.__e,(m.ref!=h.ref||8&m.__u)&&(m.ref&&ne(m.ref,null,h,m),h.ref&&u.push(h.ref,h.__c||g,h)),_=_||g,4&h.__u?(c=O(h,c,e,!m.__v),m.__e&&(m.__e=null)):typeof h.type==`function`&&v!==void 0?c=v:g&&(c=g.nextSibling),h.__u&=-21);return n.__e=_,c}function D(e,t,n,r,i){var a,o,s,c,l,d,f,p,h,g=n.length,_=g,v=0,x=0,C=e.__k=Array(i);for(a=0;a<i;a++)(o=t[a])!=null&&typeof o!=`boolean`&&typeof o!=`function`?(typeof o!=`object`||o.constructor===String?o=C[a]=y(null,o):m(o)?o=C[a]=y(b,{children:o}):o.constructor===u&&o.__b?o=C[a]=y(o.type,o.props,o.key,o.ref,o.__v):C[a]=o,c=a+v,o.__=e,o.__b=e.__b+1,s=null,~(l=o.__i=A(o,n,c,_))&&(_--,(s=n[l])&&(s.__u|=2)),s&&s.__v?(o.__u|=16,l==c-1?v--:l==c+1?v++:l!=c&&(l>c?v--:v++,x=1)):(~l||(i>g?v--:i<g&&v++),typeof o.type!=`function`&&(o.__u|=4))):C[a]=null;if(x){for(d=[],f=[],a=0;a<i;a++)if((o=C[a])&&16&o.__u){for(v=0,p=d.length;v<p;)d[h=v+p>>1]<o.__i?v=h+1:p=h;d[v]=o.__i,f[a]=v+1}for(v=d.length;a--;)f[a]&&(f[a]==v?v--:C[a].__u|=4)}if(_)for(a=0;a<g;a++)!(s=n[a])||2&s.__u||(s.__e==r&&(r=S(s)),re(s,s));return r}function O(e,t,n,r){var i,a,o,s;if(typeof e.type==`function`){if(e.props.__P)return t;if(i=e.__k)for(a=0;a<i.length;a++)i[a]&&(i[a].__=e,t=O(i[a],t,n,!1));return t}for(t&&!t.parentNode&&(t=S(e))&&!t.parentNode&&(t=null),o=e.__e,s=t;s&&s.nodeType==8;)s=s.nextSibling;for(o!=s&&(!r&&n.moveBefore&&o.parentNode?n.moveBefore(o,t):n.insertBefore(o,t||null)),t=o;(t=t&&t.nextSibling)&&t.nodeType==8&&!t.data.startsWith(`$s`););return t}function k(e,t){return t=t||[],e!=null&&typeof e!=`boolean`&&(m(e)?e.some(function(e){k(e,t)}):t.push(e)),t}function A(e,t,n,r){var i,a=e.key,o=e.type,s=t[n],c=s&&!(2&s.__u);if(s===null&&a==null||c&&a==s.key&&o==s.type)return n;if(r>+!!c){for(i=n-1;i>=0;i--)if((s=t[i])&&!(2&s.__u)&&a==s.key&&o==s.type)return i;for(i=n+1;i<t.length;i++)if((s=t[i])&&!(2&s.__u)&&a==s.key&&o==s.type)return i}return-1}function j(e,t,n){n??(n=``),t[0]==`-`?e.setProperty(t,n):e[t]=n}function M(e,t,n,r,i){var a,o;if(t==`style`){if(typeof n==`string`)e.style.cssText=n;else{if(typeof r==`string`&&(e.style.cssText=r=``),r)for(t in r)n&&t in n||j(e.style,t);if(n)for(t in n)r&&n[t]==r[t]||j(e.style,t,n[t])}}else if(t[0]==`o`&&t[1]==`n`)(e.__e||(e.__e={}))[t]=n,n&&r||(o=l[t]||(l[t]=N(t)),(e.__a||(e.__a={}))[t]=c,a=t!=(t=t.replace(s,`$1`)),(t=t.slice(2))[0]<`a`&&(t=t.toLowerCase()),n?e.addEventListener(t,o,a):e.removeEventListener(t,o,a));else{if(i==`http://www.w3.org/2000/svg`)t=t.replace(/xlink(H|:h)/,`h`).replace(/sName$/,`s`);else if(t!=`width`&&t!=`height`&&t!=`href`&&t!=`list`&&t!=`form`&&t!=`tabIndex`&&t!=`download`&&t!=`rowSpan`&&t!=`colSpan`&&t!=`role`&&t!=`popover`&&t in e)try{e[t]=n??``;return}catch{}if(typeof n==`function`)return;n==null||!1===n&&t[4]!=`-`?e.removeAttribute(t):e.setAttribute(t,t==`popover`&&n==1?``:n)}}function N(t){return function(n){var r=this.__e;if(r){if(r=r[t],n[o]==null)n[o]=c++;else if(n[o]<this.__a[t])return;return r(e.event?e.event(n):n)}}}function P(t,n,r,i,a,o,s,c,l,d){var p,h,v,y,C,w,T,D,O,k,A,j,M,N,P,ee,ne,ae,oe=n.type;if(n.constructor!==u)return null;128&r.__u&&(l=32&r.__u,p=r.__c.__z)&&(n.__u|=l,h=o=[],p.nodeType==8?c=F(p,o):o.push(p),c=o[0]||c),(p=e.__b)&&p(n);n:if(typeof oe==`function`){v=s.length;try{if(D=n.props,O=(p=oe.prototype)&&p.render,k=(p=oe.contextType)&&i[p.__c],A=p?k?k.props.value:p.__:i,(y=r.__c)?(n.__c=y,2&y.__g&&(y.__g|=1)):(O?n.__c=y=Line truncated
|
||||
`)>-1?e.split(`
|
||||
`):e}function zh(e,t){let{element:n,datasetIndex:r,index:i}=t,a=e.getDatasetMeta(r).controller,{label:o,value:s}=a.getLabelAndValue(i);return{chart:e,label:o,parsed:a.getParsed(i),raw:e.data.datasets[r].data[i],formattedValue:s,dataset:a.getDataset(),dataIndex:i,datasetIndex:r,element:n}}function Bh(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=ju(t.bodyFont),l=ju(t.titleFont),u=ju(t.footerFont),d=a.length,f=i.length,p=r.length,m=Au(t.padding),h=m.height,g=0,_=r.reduce((e,t)=>e+t.before.length+t.lines.length+t.after.length,0);if(_+=e.beforeBody.length+e.afterBody.length,d&&(h+=d*l.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),_){let e=t.displayColors?Math.max(s,c.lineHeight):c.lineHeight;h+=p*e+(_-p)*c.lineHeight+(_-1)*t.bodySpacing}f&&(h+=t.footerMarginTop+f*u.lineHeight+(f-1)*t.footerSpacing);let v=0,y=function(e){g=Math.max(g,n.measureText(e).width+v)};return n.save(),n.font=l.string,Mc(e.title,y),n.font=c.string,Mc(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,Mc(r,e=>{Mc(e.before,y),Mc(e.lines,y),Mc(e.after,y)}),v=0,n.font=u.string,Mc(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Vh(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function Hh(e,t,n,r){let{x:i,width:a}=r,o=n.caretSize+n.caretPadding;if(e===`left`&&i+a+o>t.width||e===`right`&&i-a-o<0)return!0}function Uh(e,t,n,r){let{x:i,width:a}=n,{width:o,chartArea:{left:s,right:c}}=e,l=`center`;return r===`center`?l=i<=(s+c)/2?`left`:`right`:i<=a/2?l=`left`:i>=o-a/2&&(l=`right`),Hh(l,e,t,n)&&(l=`center`),l}function Wh(e,t,n){let r=n.yAlign||t.yAlign||Vh(e,n);return{xAlign:n.xAlign||t.xAlign||Uh(e,t,n,r),yAlign:r}}function Gh(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Kh(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function qh(e,t,n,r){let{caretSize:i,caretPadding:a,cornerRadius:o}=e,{xAlign:s,yAlign:c}=n,l=i+a,{topLeft:u,topRight:d,bottomLeft:f,bottomRight:p}=ku(o),m=Gh(t,s),h=Kh(t,c,l);return c===`center`?s===`left`?m+=l:s===`right`&&(m-=l):s===`left`?m-=Math.max(u,f)+i:s===`right`&&(m+=Math.max(d,p)+i),{x:yl(m,0,r.width-t.width),y:yl(h,0,r.height-t.height)}}function Jh(e,t,n){let r=Au(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Yh(e){return Lh([],Rh(e))}function Xh(e,t,n){return Pu(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Zh(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Qh={beforeTitle:wc,title(e){if(e.length>0){let t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode===`dataset`)return t.dataset.label||``;if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return``},afterTitle:wc,beforeBody:wc,beforeLabel:wc,label(e){if(this&&this.options&&this.options.mode===`dataset`)return e.label+`: `+e.formattedValue||e.formattedValue;let t=e.dataset.label||``;t&&(t+=`: `);let n=e.formattedValue;return X(n)||(t+=n),t},labelColor(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:t.borderColor,backgroundColor:t.backgroundColor,borderWidth:t.borderWidth,borderDash:t.borderDash,borderDashOffset:t.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:t.pointStyle,rotation:t.rotation}},afterLabel:wc,afterBody:wc,beforeFooter:wc,footer:wc,afterFooter:wc};function $h(e,t,n,r){let i=e[t].call(n,r);return i===void 0?Qh[t].call(n,r):i}var eg=class extends Gp{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let e=this._cachedAnimations;if(e)return e;let t=this.chart,n=this.options.setContext(this.getContext()),r=n.enabled&&t.options.animation&&n.animations,i=new ef(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=Xh(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=$h(n,`beforeTitle`,this,e),i=$h(n,`title`,this,e),a=$h(n,`afterTitle`,this,e),o=[];return o=Lh(o,Rh(r)),o=Lh(o,Rh(i)),o=Lh(o,Rh(a)),o}getBeforeBody(e,t){return Yh($h(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];ret
Line truncated
|
||||
`):e}function zh(e,t){let{element:n,datasetIndex:r,index:i}=t,a=e.getDatasetMeta(r).controller,{label:o,value:s}=a.getLabelAndValue(i);return{chart:e,label:o,parsed:a.getParsed(i),raw:e.data.datasets[r].data[i],formattedValue:s,dataset:a.getDataset(),dataIndex:i,datasetIndex:r,element:n}}function Bh(e,t){let n=e.chart.ctx,{body:r,footer:i,title:a}=e,{boxWidth:o,boxHeight:s}=t,c=ju(t.bodyFont),l=ju(t.titleFont),u=ju(t.footerFont),d=a.length,f=i.length,p=r.length,m=Au(t.padding),h=m.height,g=0,_=r.reduce((e,t)=>e+t.before.length+t.lines.length+t.after.length,0);if(_+=e.beforeBody.length+e.afterBody.length,d&&(h+=d*l.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),_){let e=t.displayColors?Math.max(s,c.lineHeight):c.lineHeight;h+=p*e+(_-p)*c.lineHeight+(_-1)*t.bodySpacing}f&&(h+=t.footerMarginTop+f*u.lineHeight+(f-1)*t.footerSpacing);let v=0,y=function(e){g=Math.max(g,n.measureText(e).width+v)};return n.save(),n.font=l.string,Mc(e.title,y),n.font=c.string,Mc(e.beforeBody.concat(e.afterBody),y),v=t.displayColors?o+2+t.boxPadding:0,Mc(r,e=>{Mc(e.before,y),Mc(e.lines,y),Mc(e.after,y)}),v=0,n.font=u.string,Mc(e.footer,y),n.restore(),g+=m.width,{width:g,height:h}}function Vh(e,t){let{y:n,height:r}=t;return n<r/2?`top`:n>e.height-r/2?`bottom`:`center`}function Hh(e,t,n,r){let{x:i,width:a}=r,o=n.caretSize+n.caretPadding;if(e===`left`&&i+a+o>t.width||e===`right`&&i-a-o<0)return!0}function Uh(e,t,n,r){let{x:i,width:a}=n,{width:o,chartArea:{left:s,right:c}}=e,l=`center`;return r===`center`?l=i<=(s+c)/2?`left`:`right`:i<=a/2?l=`left`:i>=o-a/2&&(l=`right`),Hh(l,e,t,n)&&(l=`center`),l}function Wh(e,t,n){let r=n.yAlign||t.yAlign||Vh(e,n);return{xAlign:n.xAlign||t.xAlign||Uh(e,t,n,r),yAlign:r}}function Gh(e,t){let{x:n,width:r}=e;return t===`right`?n-=r:t===`center`&&(n-=r/2),n}function Kh(e,t,n){let{y:r,height:i}=e;return t===`top`?r+=n:r-=t===`bottom`?i+n:i/2,r}function qh(e,t,n,r){let{caretSize:i,caretPadding:a,cornerRadius:o}=e,{xAlign:s,yAlign:c}=n,l=i+a,{topLeft:u,topRight:d,bottomLeft:f,bottomRight:p}=ku(o),m=Gh(t,s),h=Kh(t,c,l);return c===`center`?s===`left`?m+=l:s===`right`&&(m-=l):s===`left`?m-=Math.max(u,f)+i:s===`right`&&(m+=Math.max(d,p)+i),{x:yl(m,0,r.width-t.width),y:yl(h,0,r.height-t.height)}}function Jh(e,t,n){let r=Au(n.padding);return t===`center`?e.x+e.width/2:t===`right`?e.x+e.width-r.right:e.x+r.left}function Yh(e){return Lh([],Rh(e))}function Xh(e,t,n){return Pu(e,{tooltip:t,tooltipItems:n,type:`tooltip`})}function Zh(e,t){let n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}var Qh={beforeTitle:wc,title(e){if(e.length>0){let t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode===`dataset`)return t.dataset.label||``;if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return``},afterTitle:wc,beforeBody:wc,beforeLabel:wc,label(e){if(this&&this.options&&this.options.mode===`dataset`)return e.label+`: `+e.formattedValue||e.formattedValue;let t=e.dataset.label||``;t&&(t+=`: `);let n=e.formattedValue;return X(n)||(t+=n),t},labelColor(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:t.borderColor,backgroundColor:t.backgroundColor,borderWidth:t.borderWidth,borderDash:t.borderDash,borderDashOffset:t.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){let t=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:t.pointStyle,rotation:t.rotation}},afterLabel:wc,afterBody:wc,beforeFooter:wc,footer:wc,afterFooter:wc};function $h(e,t,n,r){let i=e[t].call(n,r);return i===void 0?Qh[t].call(n,r):i}var eg=class extends Gp{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let e=this._cachedAnimations;if(e)return e;let t=this.chart,n=this.options.setContext(this.getContext()),r=n.enabled&&t.options.animation&&n.animations,i=new ef(this.chart,r);return r._cacheable&&(this._cachedAnimations=Object.freeze(i)),i}getContext(){return this.$context||(this.$context=Xh(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,t){let{callbacks:n}=t,r=$h(n,`beforeTitle`,this,e),i=$h(n,`title`,this,e),a=$h(n,`afterTitle`,this,e),o=[];return o=Lh(o,Rh(r)),o=Lh(o,Rh(i)),o=Lh(o,Rh(a)),o}getBeforeBody(e,t){return Yh($h(t.callbacks,`beforeBody`,this,e))}getBody(e,t){let{callbacks:n}=t,r=[];ret
Line truncated
|
||||
@@ -556,6 +556,93 @@ grep -Fq "$(printf 'retro-arcade\t1\t1\t0\t0\t')" "$TMP/runtime/apps-status.tsv"
|
||||
grep -Fqx "$(printf 'code\tE29')" "$TMP/runtime/apps-operation.state" || fail 'second app did not reach manual ownership check'
|
||||
printf 'PASS: multi-app lookup, E22 regression and manual package protection\n'
|
||||
|
||||
# Firmware sysupgrade drops optional APKs but keeps Apps ownership. A newer
|
||||
# firmware may approve a different APK version/hash for the same owned name.
|
||||
# Only the privileged restore path may reconcile missing owned packages.
|
||||
: > "$APPS_TEST_INSTALLED"
|
||||
: > "$APPS_TEST_VERSION_DB"
|
||||
rm -f "$TMP/owned/retro-arcade" "$TMP/owned/network-usage-monitor"
|
||||
printf 'new signed Retro payload\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk"
|
||||
RESTORE_NEW_SHA="$(sha256sum "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk" | cut -d ' ' -f 1)"
|
||||
cat > "$TMP/catalog.json" <<EOF_RESTORE_NEW
|
||||
{"schema":1,"device_code":"iptime-ax3000sm","firmware_build_id":"mock-new-firmware","channel":"stable","apps":[{"id":"retro-arcade","version":"1.1.0-r1","runtime":"router","installer":"apk","package_arch":"aarch64_cortex-a53","repository_release_id":"20261027T123000","repository_index_sha256":"$INDEX_SHA","packages":[{"name":"smartsafehub-app-retro-arcade","version":"1.1.0-r1","sha256":"$RESTORE_NEW_SHA"}],"requirements":{"min_memory_mb":1,"min_storage_mb":1}}]}
|
||||
EOF_RESTORE_NEW
|
||||
printf 'smartsafehub-app-retro-arcade\t1.0.0-r1\t%s\n' "$RETROSHA" > "$TMP/owned/retro-arcade"
|
||||
RESTORE_OLD_MANIFEST="$(cat "$TMP/owned/retro-arcade")"
|
||||
RESTORE_RECEIPTS="$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')"
|
||||
"$MANAGER" start install retro-arcade || fail 'manual install with old ownership did not queue'
|
||||
grep -Fqx "$(printf 'code\tE28')" "$TMP/runtime/apps-operation.state" || fail 'ordinary install bypassed version ownership check'
|
||||
[ "$(cat "$TMP/owned/retro-arcade")" = "$RESTORE_OLD_MANIFEST" ] || fail 'failed manual install rewrote ownership'
|
||||
# A different release that is still installed is not safe to replace.
|
||||
printf 'smartsafehub-app-retro-arcade\n' > "$APPS_TEST_INSTALLED"
|
||||
"$MANAGER" start install retro-arcade restore || fail 'installed old release restore did not queue'
|
||||
grep -Fqx "$(printf 'code\tE28')" "$TMP/runtime/apps-operation.state" || fail 'restore silently replaced a present release'
|
||||
[ "$(cat "$TMP/owned/retro-arcade")" = "$RESTORE_OLD_MANIFEST" ] || fail 'present package changed ownership'
|
||||
: > "$APPS_TEST_INSTALLED"
|
||||
# Missing dependencies and a tampered APK must fail before re-pinning records.
|
||||
export APPS_TEST_MISSING_DEP=1
|
||||
"$MANAGER" start install retro-arcade restore || fail 'dependency simulation failure did not queue'
|
||||
grep -Fqx "$(printf 'code\tE34')" "$TMP/runtime/apps-operation.state" || fail 'restore bypassed solver guard'
|
||||
[ "$(cat "$TMP/owned/retro-arcade")" = "$RESTORE_OLD_MANIFEST" ] || fail 'failed solver changed ownership'
|
||||
unset APPS_TEST_MISSING_DEP
|
||||
printf 'tampered new Retro payload\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk"
|
||||
"$MANAGER" start install retro-arcade restore || fail 'tampered restore did not queue'
|
||||
grep -Fqx "$(printf 'code\tE33')" "$TMP/runtime/apps-operation.state" || fail 'tampered new APK passed restore'
|
||||
[ "$(cat "$TMP/owned/retro-arcade")" = "$RESTORE_OLD_MANIFEST" ] || fail 'tampered APK re-pinned ownership'
|
||||
printf 'new signed Retro payload\n' > "$TMP/repo/smartsafehub-app-retro-arcade-1.1.0-r1.apk"
|
||||
# A package omitted from a new approval cannot be rescued by stale ownership.
|
||||
printf 'unapproved-old-package\t1.0.0-r1\t%s\n' "$RETROSHA" >> "$TMP/owned/retro-arcade"
|
||||
"$MANAGER" start install retro-arcade restore || fail 'unapproved old owner check did not queue'
|
||||
grep -Fqx "$(printf 'code\tE28')" "$TMP/runtime/apps-operation.state" || fail 'unapproved old package re-pinned'
|
||||
printf '%s\n' "$RESTORE_OLD_MANIFEST" > "$TMP/owned/retro-arcade"
|
||||
printf '%s\n' "$RESTORE_OLD_MANIFEST" >> "$TMP/owned/retro-arcade"
|
||||
"$MANAGER" start install retro-arcade restore || fail 'duplicate ownership check did not queue'
|
||||
grep -Fqx "$(printf 'code\tE28')" "$TMP/runtime/apps-operation.state" || fail 'duplicate ownership authorized restore'
|
||||
printf '%s\n' "$RESTORE_OLD_MANIFEST" > "$TMP/owned/retro-arcade"
|
||||
# An unterminated or empty ownership record cannot authorize restoration.
|
||||
printf '%s' "$RESTORE_OLD_MANIFEST" > "$TMP/owned/retro-arcade"
|
||||
"$MANAGER" start install retro-arcade restore || fail 'truncated ownership check did not queue'
|
||||
grep -Fqx "$(printf 'code\tE28')" "$TMP/runtime/apps-operation.state" || fail 'truncated ownership authorized restore'
|
||||
printf '%s\n' "$RESTORE_OLD_MANIFEST" > "$TMP/owned/retro-arcade"
|
||||
# A preinstalled system conntrack package must remain independent of Apps.
|
||||
printf 'conntrack\n' > "$APPS_TEST_INSTALLED"
|
||||
"$MANAGER" start install retro-arcade restore || fail 'approved new firmware restore not queued'
|
||||
grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-operation.state" || fail 'new approved release was not restored'
|
||||
grep -Fqx "$(printf 'smartsafehub-app-retro-arcade\t1.1.0-r1\t%s' "$RESTORE_NEW_SHA")" "$TMP/owned/retro-arcade" || fail 'ownership was not updated to newly approved version'
|
||||
grep -Fqx conntrack "$APPS_TEST_INSTALLED" || fail 'restore removed preinstalled conntrack'
|
||||
[ "$(wc -l < "$APPS_TEST_INSTALLED" | tr -d ' ')" -eq 2 ] || fail 'restore installed unrelated packages'
|
||||
[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq "$RESTORE_RECEIPTS" ] || fail 'automatic restore counted as user install'
|
||||
"$MANAGER" start install retro-arcade restore || fail 'second restore not queued'
|
||||
grep -Fqx "$(printf 'code\tE28')" "$TMP/runtime/apps-operation.state" || fail 'already restored app was installed twice'
|
||||
# Ownership files cannot turn conntrack into a removable Apps package.
|
||||
printf 'conntrack\t1.0-r1\t%s\n' "$RETROSHA" > "$TMP/owned/retro-arcade"
|
||||
"$MANAGER" start remove retro-arcade || fail 'protected package remove did not queue'
|
||||
grep -Fqx "$(printf 'code\tE42')" "$TMP/runtime/apps-operation.state" || fail 'conntrack was not protected during removal'
|
||||
grep -Fqx conntrack "$APPS_TEST_INSTALLED" || fail 'protected conntrack was removed'
|
||||
rm -f "$TMP/owned/retro-arcade"
|
||||
# Multi-package restore must preserve existing ownership even when package
|
||||
# records appear in a different order. It may re-pin the one missing APK but
|
||||
# must never reinstall the other two or claim the system conntrack package.
|
||||
cat > "$TMP/catalog.json" <<EOF_RESTORE_MULTI
|
||||
{"schema":1,"device_code":"iptime-ax3000sm","firmware_build_id":"mock-new-firmware","channel":"stable","apps":[{"id":"network-usage-monitor","version":"2025.06.02-r1","runtime":"router","installer":"apk","package_arch":"aarch64_cortex-a53","repository_release_id":"20261027T123000","repository_index_sha256":"$INDEX_SHA","packages":[{"name":"nlbwmon","version":"2025.06.02~29236be6-r1","sha256":"$ARCHIVE_SHA"},{"name":"luci-app-nlbwmon","version":"26.288.56989~75a306d","sha256":"$LUCISH"},{"name":"luci-lib-chartjs","version":"26.288.56989~75a306d","sha256":"$CHARTSHA"}],"requirements":{"min_memory_mb":1,"min_storage_mb":1}}]}
|
||||
EOF_RESTORE_MULTI
|
||||
printf 'luci-app-nlbwmon\n' >> "$APPS_TEST_INSTALLED"
|
||||
printf 'nlbwmon\n' >> "$APPS_TEST_INSTALLED"
|
||||
cat > "$TMP/owned/network-usage-monitor" <<EOF_RESTORE_OWNED
|
||||
luci-app-nlbwmon 26.288.56989~75a306d $LUCISH
|
||||
nlbwmon 2025.06.02~29236be6-r1 $ARCHIVE_SHA
|
||||
luci-lib-chartjs 26.288.56989~75a306d $RETROSHA
|
||||
EOF_RESTORE_OWNED
|
||||
"$MANAGER" start install network-usage-monitor restore || fail 'partial multi-package restore not queued'
|
||||
grep -Fqx "$(printf 'phase\tsuccess')" "$TMP/runtime/apps-operation.state" || fail 'partial multi-package restore failed'
|
||||
[ "$(wc -l < "$TMP/owned/network-usage-monitor" | tr -d ' ')" -eq 3 ] || fail 'partial restore duplicated owned packages'
|
||||
grep -Fqx "$(printf 'luci-lib-chartjs\t26.288.56989~75a306d\t%s' "$CHARTSHA")" "$TMP/owned/network-usage-monitor" || fail 'partial restore did not re-pin missing chart package'
|
||||
[ "$(wc -l < "$APPS_TEST_INSTALLED" | tr -d ' ')" -eq 5 ] || fail 'partial restore modified present packages'
|
||||
grep -Fqx conntrack "$APPS_TEST_INSTALLED" || fail 'partial restore modified conntrack'
|
||||
[ "$(wc -l < "$APPS_TEST_METRICS_LOG" | tr -d ' ')" -eq "$RESTORE_RECEIPTS" ] || fail 'partial restore counted as new user install'
|
||||
rm -f "$TMP/owned/network-usage-monitor"
|
||||
printf 'PASS: new-firmware Apps re-pinning, signed restore, no adoption, and conntrack protection\n'
|
||||
|
||||
# An Apps-owned app can update to a newer approved, signed package without
|
||||
# reinstalling, touching system packages or generating a new install receipt.
|
||||
: > "$APPS_TEST_INSTALLED"
|
||||
|
||||
@@ -153,7 +153,7 @@ def self_test() -> None:
|
||||
workflow_path = root / '.github/workflows/ci.yml'
|
||||
workflow_base = '''jobs:
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Guard
|
||||
run: sh tests/test-suite-integrity.sh
|
||||
@@ -162,7 +162,7 @@ def self_test() -> None:
|
||||
- name: OpenWrt BusyBox
|
||||
run: sh tests/test-openwrt-commands.sh --self-test
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Test
|
||||
run: node --experimental-strip-types --test ../tests/runtime/*.test.mjs
|
||||
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=198
|
||||
PKG_RELEASE:=99
|
||||
PKG_RELEASE:=100
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
@@ -218,6 +218,7 @@ local function strip_runtime_conflicts(doc)
|
||||
doc.port = nil
|
||||
doc["external-controller"] = nil
|
||||
doc.secret = nil
|
||||
doc.authentication = nil
|
||||
doc["allow-lan"] = nil
|
||||
if type(doc.dns) == "table" then
|
||||
doc.dns["fake-ip-range"] = nil
|
||||
@@ -323,6 +324,7 @@ local function merge(raw_path, overlay_path, output_path)
|
||||
local filled_groups = fill_empty_proxy_groups(raw_doc)
|
||||
local stripped_rules = strip_incompatible_script_rules(raw_doc)
|
||||
local merged = deep_merge(raw_doc, overlay_doc)
|
||||
strip_runtime_conflicts(merged)
|
||||
local ok, rendered = pcall(lyaml.dump, { merged })
|
||||
if not ok or not rendered then
|
||||
io.stderr:write("dump_failed\n")
|
||||
@@ -740,6 +742,115 @@ local function apply_trojan_tls_options(proxy, sid)
|
||||
end
|
||||
end
|
||||
|
||||
-- Xray camel → mihomo kebab(反向字段表)
|
||||
local XHTTP_FIELD_MAP = {
|
||||
headers = "headers",
|
||||
xPaddingBytes = "x-padding-bytes",
|
||||
noGRPCHeader = "no-grpc-header",
|
||||
xPaddingObfsMode = "x-padding-obfs-mode",
|
||||
xPaddingKey = "x-padding-key",
|
||||
xPaddingHeader = "x-padding-header",
|
||||
xPaddingPlacement = "x-padding-placement",
|
||||
xPaddingMethod = "x-padding-method",
|
||||
uplinkHTTPMethod = "uplink-http-method",
|
||||
sessionIDPlacement = "session-placement",
|
||||
sessionIDKey = "session-key",
|
||||
sessionIDTable = "session-table",
|
||||
sessionIDLength = "session-length",
|
||||
seqPlacement = "seq-placement",
|
||||
seqKey = "seq-key",
|
||||
uplinkDataPlacement = "uplink-data-placement",
|
||||
uplinkDataKey = "uplink-data-key",
|
||||
uplinkChunkSize = "uplink-chunk-size",
|
||||
scMaxEachPostBytes = "sc-max-each-post-bytes",
|
||||
scMinPostsIntervalMs = "sc-min-posts-interval-ms",
|
||||
}
|
||||
|
||||
-- xmux 内部 camel → kebab
|
||||
local XMUX_FIELD_MAP = {
|
||||
maxConcurrency = "max-concurrency",
|
||||
maxConnections = "max-connections",
|
||||
cMaxReuseTimes = "c-max-reuse-times",
|
||||
hMaxRequestTimes = "h-max-request-times",
|
||||
hMaxReusableSecs = "h-max-reusable-secs",
|
||||
hKeepAlivePeriod = "h-keep-alive-period",
|
||||
}
|
||||
|
||||
local function convert_xhttp_pair(key, value)
|
||||
-- xmux → reuse-settings
|
||||
if key == "xmux" and type(value) == "table" then
|
||||
local out = {}
|
||||
for k, v in pairs(value) do
|
||||
local mapped = XMUX_FIELD_MAP[k]
|
||||
if mapped then
|
||||
out[mapped] = v
|
||||
end
|
||||
end
|
||||
if next(out) == nil then return nil, nil end
|
||||
return "reuse-settings", out
|
||||
end
|
||||
|
||||
-- downloadSettings → download-settings(只保留 xhttp 部分)
|
||||
if key == "downloadSettings" and type(value) == "table" then
|
||||
local out = {}
|
||||
for k, v in pairs(value) do
|
||||
if k == "xmux" then
|
||||
local nk, nv = convert_xhttp_pair(k, v)
|
||||
if nk then out[nk] = nv end
|
||||
elseif k == "path" or k == "host" then
|
||||
out[k] = v
|
||||
elseif XHTTP_FIELD_MAP[k] then
|
||||
if not (k == "headers" and type(v) == "table" and next(v) == nil) then
|
||||
out[XHTTP_FIELD_MAP[k]] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
if next(out) == nil then return nil, nil end
|
||||
return "download-settings", out
|
||||
end
|
||||
|
||||
if key == "headers" and type(value) == "table" then
|
||||
if next(value) == nil then
|
||||
return nil, nil
|
||||
end
|
||||
return "headers", value
|
||||
end
|
||||
|
||||
local mapped = XHTTP_FIELD_MAP[key]
|
||||
if mapped then
|
||||
return mapped, value
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local function apply_xhttp_extra(opts, sid)
|
||||
if get_server_field(sid, "enable_xhttp_extra", "0") ~= "1" then
|
||||
return
|
||||
end
|
||||
local raw = get_server_field(sid, "xhttp_extra", "")
|
||||
if raw == "" or not ok_jsonc or not jsonc then
|
||||
return
|
||||
end
|
||||
|
||||
local decoded = nixio.bin.b64decode(raw)
|
||||
if not decoded then
|
||||
return
|
||||
end
|
||||
|
||||
local extra = jsonc.parse(decoded)
|
||||
if type(extra) ~= "table" then
|
||||
return
|
||||
end
|
||||
|
||||
for k, v in pairs(extra) do
|
||||
local new_key, new_val = convert_xhttp_pair(k, v)
|
||||
if new_key then
|
||||
opts[new_key] = new_val
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function apply_v2ray_transport_options(proxy, sid)
|
||||
local transport = get_server_field(sid, "transport", "raw")
|
||||
if transport == "raw" or transport == "tcp" or transport == "" then
|
||||
@@ -782,6 +893,7 @@ local function apply_v2ray_transport_options(proxy, sid)
|
||||
host = string_or_nil(get_server_field(sid, "xhttp_host", "")),
|
||||
mode = string_or_nil(get_server_field(sid, "xhttp_mode", ""))
|
||||
}
|
||||
apply_xhttp_extra(proxy["xhttp-opts"], sid)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-vantage
|
||||
PKG_VERSION:=1.0.15
|
||||
PKG_RELEASE:=28
|
||||
PKG_VERSION:=1.0.16
|
||||
PKG_RELEASE:=29
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage dashboard
|
||||
|
||||
@@ -385,6 +385,8 @@ html[data-darkmode="true"] .vt-app {
|
||||
}
|
||||
.vt-app .vt-editbar[hidden] { display: none; }
|
||||
.vt-app .vt-editbar-text { display: flex; flex-direction: column; gap: 2px; flex: 1 1 260px; min-width: 0; }
|
||||
.vt-app .vt-edit-note { font-size: var(--vt-fs-sm); color: var(--vt-text-2); }
|
||||
.vt-app .vt-edit-note[hidden] { display: none; }
|
||||
.vt-app .vt-editbar-text strong { font-size: var(--vt-fs-base); font-weight: var(--vt-fw-semibold); }
|
||||
.vt-app .vt-edit-hidden { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||
.vt-app .vt-edit-hidden:empty { display: none; }
|
||||
@@ -801,10 +803,10 @@ h2, h3{
|
||||
|
||||
.vt-app .vt-link-label {
|
||||
position: relative; z-index: var(--vt-z-raised);
|
||||
display: grid; grid-template-rows: minmax(0, 1fr) 14px minmax(0, 1fr); justify-items: center;
|
||||
display: grid; grid-template-rows: minmax(0, 1fr) 14px minmax(0, 1fr); grid-template-columns: minmax(0, 1fr); justify-items: center;
|
||||
align-self: stretch; padding: 0 6px; text-align: center;
|
||||
}
|
||||
.vt-app .vt-link-below { grid-row: 3; align-self: start; display: flex; flex-direction: column; align-items: center; }
|
||||
.vt-app .vt-link-below { grid-row: 3; align-self: start; display: flex; flex-direction: column; align-items: center; max-width: 100%; min-width: 0; }
|
||||
.vt-app .vt-link-rate {
|
||||
display: inline-flex; align-items: baseline; gap: 3px; white-space: nowrap;
|
||||
padding: 0 6px; border-radius: 6px; background: var(--vt-surface);
|
||||
@@ -817,14 +819,14 @@ h2, h3{
|
||||
.vt-app .vt-link-up { font-size: var(--vt-fs-sm); }
|
||||
.vt-app .vt-link-up b { font-size: var(--vt-fs-base); font-weight: var(--vt-fw-semibold); color: var(--vt-text-2); }
|
||||
.vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); }
|
||||
.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px;
|
||||
.vt-app .vt-link-meta { font-size: var(--vt-fs-micro); color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
|
||||
padding: 0 6px; border-radius: 6px; background: var(--vt-surface); }
|
||||
/* a full-tunnel VPN over the main line: its wire drawn as a pipe in its
|
||||
own colour around the traffic, the tunnel named under the line's rates */
|
||||
.vt-app .vt-wire-tunnel { stroke-width: 9; stroke-linecap: round; stroke: color-mix(in oklab, var(--vt-tunnel) 24%, transparent); }
|
||||
.vt-app .vt-wire-tunnel.vt-wire-on { stroke: color-mix(in oklab, var(--vt-tunnel) 36%, transparent); }
|
||||
.vt-app .vt-link-tunnel {
|
||||
display: inline-flex; align-items: center; gap: 4px; max-width: 100%; margin-top: 4px; padding: 1px 6px;
|
||||
display: inline-flex; align-items: center; gap: 4px; max-width: 100%; min-width: 0; margin-top: 4px; padding: 1px 6px;
|
||||
border: 1px solid color-mix(in oklab, var(--vt-tunnel) 60%, var(--vt-line)); border-radius: 6px;
|
||||
background: var(--vt-surface); color: var(--vt-text-2); font-size: var(--vt-fs-micro); white-space: nowrap;
|
||||
}
|
||||
@@ -853,8 +855,8 @@ h2, h3{
|
||||
.vt-app .vt-node-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
/* the main line among others: the default route the kernel takes */
|
||||
.vt-app .vt-node-tag {
|
||||
display: inline-flex; align-items: center; height: 16px; padding: 0 5px; border-radius: 4px;
|
||||
text-transform: none; letter-spacing: 0; font-weight: var(--vt-fw-medium);
|
||||
display: inline-block; height: 16px; line-height: 16px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
padding: 0 5px; border-radius: 4px; text-transform: none; letter-spacing: 0; font-weight: var(--vt-fw-medium);
|
||||
background: var(--vt-accent-soft); color: var(--vt-accent-text);
|
||||
}
|
||||
|
||||
@@ -865,6 +867,10 @@ h2, h3{
|
||||
.vt-app .vt-branch-ssids { display: flex; flex-direction: column; gap: var(--vt-sp-2); min-width: 0; }
|
||||
.vt-app .vt-branch-ssids .vt-node { max-width: 260px; }
|
||||
|
||||
/* medium widths: the rates need their column, and the branches have room to give */
|
||||
@container vthero (max-width: 1020px) {
|
||||
.vt-app .vt-path { grid-template-columns: minmax(150px, 1.05fr) minmax(136px, .9fr) minmax(170px, 1.1fr) minmax(204px, 2fr); }
|
||||
}
|
||||
@container vthero (max-width: 700px) {
|
||||
.vt-app .vt-path { grid-template-columns: minmax(0, 1fr); gap: 12px; min-height: 0; }
|
||||
.vt-app .vt-node-gw, .vt-app .vt-node-ap { justify-self: start; min-width: 220px; }
|
||||
|
||||
@@ -111,7 +111,8 @@ return baseclass.extend({
|
||||
|
||||
/* a layout as that option's value: 'hero:0:4 chart:0:2:h' */
|
||||
encode: function(list) {
|
||||
return list.map(function(e) { return e.id + ':' + e.c + ':' + e.w + (e.hide ? ':h' : ''); }).join(' ');
|
||||
return list.filter(function(e) { return e && typeof e === 'object'; })
|
||||
.map(function(e) { return e.id + ':' + e.c + ':' + e.w + (e.hide ? ':h' : ''); }).join(' ');
|
||||
},
|
||||
|
||||
/* the template's data-vt-look-layout, 'wide=hero:0:4 chart:0:2:h;mid=default',
|
||||
|
||||
@@ -34,6 +34,8 @@ var MAX_LINES = 4;
|
||||
are left out: some ISPs give their access through them. */
|
||||
var TUNNEL = /^(wireguard|openvpn|openconnect|openfortivpn|vpnc|sstp|gre|gretap|grev6|grev6tap|vti|vtiv6|xfrm|ipip|6in4|6to4|6rd|vxlan|vxlan6)$/;
|
||||
var TUNNEL_DEV = /^(wireguard|tun|tap|ipip|ip6tnl|sit|gre|gretap|ip6gre|ip6gretap|vti|vti6|xfrm|vxlan)$/;
|
||||
/* IPv6 carried over the IPv4 line: part of that line, not a way out or a VPN */
|
||||
var IPV6_OVER = /^(6in4|6rd|6to4)$/;
|
||||
|
||||
/* netifd lists a route's metric only when the route sets one; the
|
||||
interface's metric applies otherwise. The kernel stores an IPv6
|
||||
@@ -72,17 +74,22 @@ function isTunnel(i, devs) {
|
||||
/* the interfaces with a default route, the line first: real lines
|
||||
before tunnels (a full-tunnel VPN rides over a line, see ridingTunnel),
|
||||
IPv4 before IPv6 (a softwire's IPv4 route among the IPv4 ones), then
|
||||
the lowest metric, wan/wan6/*wan* on a tie, then the order netifd lists
|
||||
the lowest metric, on a tie the one the kernel says it takes (`kernel`,
|
||||
as ridingTunnel's; netifd replaces routes of equal metric, so that is
|
||||
the last one added), then wan/wan6/*wan*, then the order netifd lists
|
||||
them */
|
||||
function rankRouted(up, devs) {
|
||||
function rankRouted(up, devs, kernel) {
|
||||
var taken = {};
|
||||
arr(kernel).forEach(function(x) { if (x && typeof x.dev === 'string') taken[x.family === 6 ? 1 : 0] = x.dev; });
|
||||
return up.map(function(i, n) {
|
||||
var r = defRoute(i);
|
||||
/* a softwire's device is a tunnel's (ip6tnl), but it carries the line's IPv4, not a VPN */
|
||||
var sw = SOFTWIRE.test(i.proto || '');
|
||||
return r ? { i: i, tunnel: !sw && isTunnel(i, devs), softwire: sw, family: r.target === '::' ? 1 : 0,
|
||||
metric: metric(i, r), wan: wanRank(i), n: n } : null;
|
||||
var family = r && r.target === '::' ? 1 : 0;
|
||||
return r ? { i: i, tunnel: !sw && isTunnel(i, devs), softwire: sw, family: family,
|
||||
metric: metric(i, r), taken: taken[family] === (i.l3_device || i.device) ? 0 : 1, wan: wanRank(i), n: n } : null;
|
||||
}).filter(Boolean).sort(function(a, b) {
|
||||
return a.tunnel - b.tunnel || a.family - b.family || a.metric - b.metric || a.wan - b.wan || a.n - b.n;
|
||||
return a.tunnel - b.tunnel || a.family - b.family || a.metric - b.metric || a.taken - b.taken || a.wan - b.wan || a.n - b.n;
|
||||
});
|
||||
}
|
||||
/* A full-tunnel VPN over the line: the tunnel holding the default route
|
||||
@@ -123,7 +130,7 @@ function ridingTunnel(ifaces, ranked, devs, kernel) {
|
||||
parent device (two PPPoE sessions on eth0) stay two. The main line
|
||||
carries the full-tunnel VPN over it, if any (tunnel; ridingTunnel). */
|
||||
function groupLines(ifaces, devs, kernel) {
|
||||
var ranked = rankRouted(upIfaces(ifaces), devs), main = ranked[0], groups = [];
|
||||
var ranked = rankRouted(upIfaces(ifaces), devs, kernel), main = ranked[0], groups = [];
|
||||
if (!main) return groups;
|
||||
var at = main.metric, rider = main.tunnel ? null : ridingTunnel(ifaces, ranked, devs, kernel);
|
||||
if (main.softwire)
|
||||
@@ -148,6 +155,9 @@ function groupLines(ifaces, devs, kernel) {
|
||||
if (g) add(g, r);
|
||||
else start(r, r.metric);
|
||||
});
|
||||
/* with several lines there's no telling which one it rides */
|
||||
if (groups.length === 1)
|
||||
ranked.forEach(function(r) { if (r !== main && r.tunnel && r.family === 1 && IPV6_OVER.test(r.i.proto || '')) add(groups[0], r); });
|
||||
return groups;
|
||||
}
|
||||
function cidrs(list) {
|
||||
@@ -172,7 +182,7 @@ function ownConduits(lines) {
|
||||
var shared = lines.map(function(x, k) {
|
||||
return x.statsDev !== x.dev && lines.some(function(y, j) { return j !== k && (y.statsDev === x.statsDev || y.dev === x.statsDev); });
|
||||
});
|
||||
lines.forEach(function(x, k) { if (shared[k]) x.statsDev = x.dev; });
|
||||
lines.forEach(function(x, k) { if (shared[k]) { x.statsDev = x.dev; x.stats = x.ownStats; } });
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -403,19 +413,20 @@ return baseclass.extend({
|
||||
/* The other lines to the internet beside `main` (resolveUplink's):
|
||||
every other up interface with a default route in the main table
|
||||
(failover by metric, mwan3), in the kernel's order, as groupLines
|
||||
makes them: a softwire or a tunnel (WireGuard, OpenVPN, GRE,
|
||||
6in4, ...) rides over another line and is not one of its own; a
|
||||
makes them: a softwire or a tunnel (WireGuard, OpenVPN, GRE, ...)
|
||||
rides over another line and is not one of its own; 6rd, 6in4 or
|
||||
6to4 IPv6 is part of the line when there is only one; a
|
||||
default in another table (policy routing) is not ordinary
|
||||
traffic's. A WAN that is down has no route: it stays a network
|
||||
until it comes up. On a bridge, `viaOf(next hop)` names the port
|
||||
the bridge learned that gateway on; without it a bridge with
|
||||
several wired ports keeps its own counters.
|
||||
-> [ line, ... ] as resolveUplink's, without main */
|
||||
extraUplinks: function(ifaces, devs, wirelessNames, main, viaOf) {
|
||||
extraUplinks: function(ifaces, devs, wirelessNames, main, viaOf, kernel) {
|
||||
var self = this;
|
||||
if (!main) return [];
|
||||
devs = obj(devs);
|
||||
return groupLines(ifaces, devs).slice(1).filter(function(g) {
|
||||
return groupLines(ifaces, devs, kernel).filter(function(g) {
|
||||
return g.l3.indexOf(main.l3dev) < 0 && !g.members.some(function(i) { return i.interface === main.iface; });
|
||||
}).map(function(g) {
|
||||
var r = defRoute(g.r.i), hop = r && r.nexthop;
|
||||
@@ -448,8 +459,12 @@ return baseclass.extend({
|
||||
}
|
||||
}
|
||||
|
||||
var statsDev = dev;
|
||||
if (d && d.devtype === 'dsa' && d['hw-tc-offload'] && d.conduit && devs[d.conduit])
|
||||
/* PPPoE: flow offloading takes downloads past the session's own
|
||||
counters, and the port under it counts everything (#92). Lines
|
||||
that share the port go back to their own (ownConduits). */
|
||||
var under = pick.proto === 'pppoe' && typeof pick.device === 'string' && pick.device !== l3 && devs[pick.device] ? pick.device : null;
|
||||
var counted = under ? devs[under] : d, statsDev = under || dev;
|
||||
if (!under && d && d.devtype === 'dsa' && d['hw-tc-offload'] && d.conduit && devs[d.conduit])
|
||||
statsDev = d.conduit;
|
||||
|
||||
var route = defRoute(pick), hop = route && route.nexthop;
|
||||
@@ -474,7 +489,8 @@ return baseclass.extend({
|
||||
dns: arr(pick['dns-server']).filter(function(s) { return typeof s === 'string'; }),
|
||||
/* the interfaces the line is made of (wan, wan6) */
|
||||
ifaces: [ pick.interface ],
|
||||
stats: d && d.statistics ? d.statistics : null
|
||||
stats: counted && counted.statistics ? counted.statistics : null,
|
||||
ownStats: d && d.statistics ? d.statistics : null
|
||||
};
|
||||
},
|
||||
|
||||
@@ -705,7 +721,7 @@ return baseclass.extend({
|
||||
var uplinks = uplink ? ownConduits([ uplink ].concat(this.extraUplinks(raw.ifaces, devs, wlNames, uplink, function(hop) {
|
||||
var mac = macOf(hop);
|
||||
return mac && fdb[mac] ? fdb[mac].port : null;
|
||||
}).slice(0, MAX_LINES))) : [];
|
||||
}, kernel).slice(0, MAX_LINES))) : [];
|
||||
/* the network on each bridge (or bridge VLAN: br-lan.10), and its
|
||||
IPv4 subnets for the hosts' addresses */
|
||||
var lans = Object.create(null);
|
||||
|
||||
@@ -499,6 +499,25 @@ function lookAttr(name) {
|
||||
|
||||
function lookSaves() { return lookAttr('data-vt-look-save') === 'yes'; }
|
||||
|
||||
/* What Customize changed and the router doesn't have yet, as the options
|
||||
it's saved in. Kept across a failed save or a reload, so Done sends
|
||||
this and nothing else: a browser's earlier picks stay its own until
|
||||
someone changes them. */
|
||||
var UNSAVED_KEY = 'vantage.view.unsaved';
|
||||
|
||||
function readUnsaved() {
|
||||
var v = null;
|
||||
try { v = JSON.parse(window.localStorage.getItem(UNSAVED_KEY) || 'null'); } catch (e) { v = null; }
|
||||
return Array.isArray(v) ? v.filter(function(k) { return /^(chart|layout_(wide|mid|narrow)(_wired)?)$/.test(k); }) : [];
|
||||
}
|
||||
|
||||
function writeUnsaved(list) {
|
||||
try {
|
||||
if (list.length) window.localStorage.setItem(UNSAVED_KEY, JSON.stringify(list));
|
||||
else window.localStorage.removeItem(UNSAVED_KEY);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/* Done hands the theme what changed; true if it took it (a login that may
|
||||
save). Without it, or for any other login, it stays in this browser. */
|
||||
function offerView(values, done) {
|
||||
@@ -517,12 +536,21 @@ function readLayouts() {
|
||||
var out = Object.create(null);
|
||||
if (v && typeof v === 'object' && !Array.isArray(v))
|
||||
Object.keys(v).forEach(function(k) { if (/^(wide|mid|narrow)(-wired)?$/.test(k) && Array.isArray(v[k])) out[k] = v[k]; });
|
||||
return layout.merge(out, layout.decode(lookAttr('data-vt-look-layout')), lookSaves());
|
||||
/* changed here and not saved yet: this browser's stands, until Done sends it */
|
||||
var saved = layout.decode(lookAttr('data-vt-look-layout')), unsaved = readUnsaved();
|
||||
Object.keys(saved).forEach(function(k) { if (unsaved.indexOf(layout.option(k)) >= 0) delete saved[k]; });
|
||||
return layout.merge(out, saved, lookSaves());
|
||||
}
|
||||
|
||||
/* a login that can't save follows the router: what is the router's isn't
|
||||
this browser's to keep */
|
||||
function writeLayouts(v) {
|
||||
var saved = lookSaves() ? null : layout.decode(lookAttr('data-vt-look-layout')), out = {};
|
||||
Object.keys(v).forEach(function(k) {
|
||||
if (!saved || !saved[k] || layout.encode(saved[k]) !== layout.encode(v[k])) out[k] = v[k];
|
||||
});
|
||||
try {
|
||||
if (Object.keys(v).length) window.localStorage.setItem(LAYOUT_KEY, JSON.stringify(v));
|
||||
if (Object.keys(out).length) window.localStorage.setItem(LAYOUT_KEY, JSON.stringify(out));
|
||||
else window.localStorage.removeItem(LAYOUT_KEY);
|
||||
} catch (e) {}
|
||||
}
|
||||
@@ -534,7 +562,7 @@ var CHART_KEY = 'vantage.chart.v1';
|
||||
function readChartStyle() {
|
||||
var v = null;
|
||||
try { v = window.localStorage.getItem(CHART_KEY); } catch (e) { v = null; }
|
||||
return series.chartPick(lookAttr('data-vt-look-chart'), v, lookSaves());
|
||||
return series.chartPick(lookAttr('data-vt-look-chart'), v, lookSaves() && readUnsaved().indexOf('chart') < 0);
|
||||
}
|
||||
|
||||
function writeChartStyle(v) {
|
||||
@@ -1286,13 +1314,22 @@ return view.extend({
|
||||
var l = this.lay;
|
||||
if (!l) return;
|
||||
var k = layout.key(l.tier, l.wired);
|
||||
if (layout.same(list, layout.defaults(l.tier, l.wired))) delete this.layouts[k];
|
||||
if (layout.same(list, layout.defaults(l.tier, l.wired))) this.clearLayout(k);
|
||||
else this.layouts[k] = layout.compact(list);
|
||||
writeLayouts(this.layouts);
|
||||
this.viewChanged(layout.option(k));
|
||||
this.held = Object.create(null);
|
||||
this.doLayout();
|
||||
},
|
||||
|
||||
/* back to the default. The router's layout would come back on reload
|
||||
(for a login that can't save, or before Done), so this browser keeps
|
||||
an empty one of its own, which normalize reads as the default. */
|
||||
clearLayout: function(k) {
|
||||
if (k in layout.decode(lookAttr('data-vt-look-layout'))) this.layouts[k] = [];
|
||||
else delete this.layouts[k];
|
||||
},
|
||||
|
||||
editTile: function(id, fn, say) {
|
||||
var l = this.lay;
|
||||
if (!l) return;
|
||||
@@ -1308,8 +1345,10 @@ return view.extend({
|
||||
window.setTimeout(function() { txt(s, text); }, 30);
|
||||
},
|
||||
|
||||
setEditing: function(on) {
|
||||
if (!on && this.editing && this.saveView()) return;
|
||||
/* leave: close without saving (Esc or the Customize button after a
|
||||
failed save; Done tries again) */
|
||||
setEditing: function(on, leave) {
|
||||
if (!on && this.editing && !leave && this.saveView()) return;
|
||||
this.editing = !!on;
|
||||
this.el.root.classList.toggle('vt-editing', this.editing);
|
||||
this.el.layoutBtn.setAttribute('aria-pressed', String(this.editing));
|
||||
@@ -1319,7 +1358,7 @@ return view.extend({
|
||||
this.held = Object.create(null);
|
||||
this.doLayout();
|
||||
if (this.editing) {
|
||||
this.viewStart = this.viewValues();
|
||||
this.viewFailed = false;
|
||||
this.editNote(lookAttr('data-vt-look-save') === 'relogin' ? _('Log in again to save this for every browser.') : '');
|
||||
this.el.editDone.focus();
|
||||
}
|
||||
@@ -1337,24 +1376,34 @@ return view.extend({
|
||||
[ 'wide', 'mid', 'narrow' ].forEach(function(tier) {
|
||||
[ false, true ].forEach(function(wired) {
|
||||
var k = layout.key(tier, wired);
|
||||
out[layout.option(k)] = self.layouts[k] ? layout.encode(self.layouts[k]) : 'default';
|
||||
out[layout.option(k)] = self.layouts[k] && self.layouts[k].length ? layout.encode(self.layouts[k]) : 'default';
|
||||
});
|
||||
});
|
||||
return out;
|
||||
},
|
||||
|
||||
/* this option changed under Customize, for a login that may save it */
|
||||
viewChanged: function(opt) {
|
||||
if (!lookSaves()) return;
|
||||
var list = readUnsaved();
|
||||
if (list.indexOf(opt) < 0) writeUnsaved(list.concat(opt));
|
||||
},
|
||||
|
||||
/* Done: what changed goes to the theme in one write. True while that
|
||||
runs: Customize closes when it is saved, and stays open with a note
|
||||
if it was not, so the pick can be saved again. */
|
||||
saveView: function() {
|
||||
var self = this, now = this.viewValues(), then = this.viewStart || {}, out = {};
|
||||
var self = this, now = this.viewValues(), out = {};
|
||||
if (this.viewSaving) return true;
|
||||
Object.keys(now).forEach(function(k) { if (now[k] !== then[k]) out[k] = now[k]; });
|
||||
readUnsaved().forEach(function(k) { out[k] = now[k]; });
|
||||
if (!Object.keys(out).length) return false;
|
||||
this.viewSaving = offerView(out, function(ok) {
|
||||
self.viewSaving = false;
|
||||
self.viewFailed = !ok;
|
||||
if (!ok) { self.editNote(_('Could not save this for every browser. Try again.')); return; }
|
||||
self.viewStart = now;
|
||||
/* anything changed since it was sent goes out on this close */
|
||||
var then = self.viewValues();
|
||||
writeUnsaved(readUnsaved().filter(function(k) { return then[k] !== out[k]; }));
|
||||
self.setEditing(false);
|
||||
});
|
||||
return this.viewSaving;
|
||||
@@ -1407,6 +1456,7 @@ return view.extend({
|
||||
if (series.CHART_STYLES.indexOf(v) < 0 || v === this.chartStyle) return;
|
||||
this.chartStyle = v;
|
||||
writeChartStyle(v);
|
||||
this.viewChanged('chart');
|
||||
this.chartHide();
|
||||
this.paintChart();
|
||||
},
|
||||
@@ -1414,7 +1464,7 @@ return view.extend({
|
||||
buildEditBar: function() {
|
||||
var self = this, el = this.el;
|
||||
el.editHint = E('span', { 'class': 'vt-hint' });
|
||||
el.editNote = E('span', { 'class': 'vt-hint', 'role': 'status', 'hidden': 'hidden' });
|
||||
el.editNote = E('span', { 'class': 'vt-edit-note', 'role': 'status', 'hidden': 'hidden' });
|
||||
el.editHidden = E('div', { 'class': 'vt-edit-hidden' });
|
||||
el.editSay = E('span', { 'class': 'vt-sr', 'role': 'status', 'aria-live': 'polite' });
|
||||
el.editBar = E('div', { 'class': 'vt-editbar', 'role': 'region', 'aria-label': _('Customize the dashboard'), 'hidden': 'hidden' }, [
|
||||
@@ -1425,8 +1475,9 @@ return view.extend({
|
||||
E('button', { 'type': 'button', 'class': 'vt-btn', 'click': function() {
|
||||
var l = self.lay;
|
||||
if (!l) return;
|
||||
delete self.layouts[layout.key(l.tier, l.wired)];
|
||||
self.clearLayout(layout.key(l.tier, l.wired));
|
||||
writeLayouts(self.layouts);
|
||||
self.viewChanged(layout.option(layout.key(l.tier, l.wired)));
|
||||
self.held = Object.create(null);
|
||||
self.doLayout();
|
||||
self.announce(_('Layout reset'));
|
||||
@@ -1607,7 +1658,7 @@ return view.extend({
|
||||
el.liveText = E('span', { 'class': 'vt-live-text' })
|
||||
]),
|
||||
el.layoutBtn = E('button', { 'type': 'button', 'class': 'vt-btn vt-btn-ghost vt-layout-btn', 'aria-pressed': 'false',
|
||||
'title': _('Customize the dashboard'), 'click': L.bind(function() { this.setEditing(!this.editing); }, this) }, [
|
||||
'title': _('Customize the dashboard'), 'click': L.bind(function() { this.setEditing(!this.editing, this.viewFailed); }, this) }, [
|
||||
icon('layout'), E('span', {}, [ _('Customize') ])
|
||||
])
|
||||
]);
|
||||
@@ -3633,7 +3684,7 @@ return view.extend({
|
||||
return;
|
||||
}
|
||||
if (ev.key === 'Escape' && this.editing && !this.drag) {
|
||||
this.setEditing(false);
|
||||
this.setEditing(false, this.viewFailed);
|
||||
return;
|
||||
}
|
||||
/* 'f' focuses the client filter; '/' and Ctrl+K belong to the theme */
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -10,8 +10,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-vantage
|
||||
PKG_VERSION:=1.0.15
|
||||
PKG_RELEASE:=34
|
||||
PKG_VERSION:=1.0.16
|
||||
PKG_RELEASE:=35
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage theme
|
||||
|
||||
@@ -1430,7 +1430,7 @@ return baseclass.extend({
|
||||
ev.preventDefault();
|
||||
if (pal.items[pal.active]) this.runEntry(pal.items[pal.active].e);
|
||||
break;
|
||||
case 'Escape': ev.preventDefault(); this.closePalette(); break;
|
||||
case 'Escape': ev.preventDefault(); ev.stopPropagation(); this.closePalette(); break;
|
||||
case 'Tab': ev.preventDefault(); break;
|
||||
}
|
||||
},
|
||||
@@ -1495,7 +1495,8 @@ return baseclass.extend({
|
||||
setLook(name, value, apply) {
|
||||
const k = LOOK[name];
|
||||
const pending = this.lookPending || (this.lookPending = {});
|
||||
if (k.ok.indexOf(value) < 0 || pending[name] === value) return;
|
||||
const failed = this.lookFailed || (this.lookFailed = {});
|
||||
if (k.ok.indexOf(value) < 0 || (pending[name] === value && !failed[name])) return;
|
||||
if (this.lookState() !== 'yes') {
|
||||
storageSet(k.own, value);
|
||||
storageSet(k.before, value);
|
||||
@@ -1504,7 +1505,7 @@ return baseclass.extend({
|
||||
}
|
||||
const seqs = this.lookSeqs || (this.lookSeqs = {}), seq = seqs[name] = (seqs[name] || 0) + 1;
|
||||
pending[name] = value;
|
||||
this.lookFailed = false;
|
||||
delete failed[name];
|
||||
apply();
|
||||
const latest = () => seqs[name] === seq;
|
||||
this.lookChain = (this.lookChain || Promise.resolve()).then(() => {
|
||||
@@ -1516,10 +1517,15 @@ return baseclass.extend({
|
||||
storageSet(LOOK_SYNC, JSON.stringify({ name: name, value: value, at: Date.now() }));
|
||||
storageSet(k.before, value);
|
||||
if (latest()) delete pending[name];
|
||||
}, () => { if (latest()) this.lookFailed = true; });
|
||||
}, () => { if (latest()) failed[name] = true; });
|
||||
}).then(apply);
|
||||
},
|
||||
|
||||
/* a save that failed and wasn't done again: its pick stays shown, with a note */
|
||||
lookFailing() {
|
||||
return Object.keys(this.lookFailed || {}).length > 0;
|
||||
},
|
||||
|
||||
themePref() {
|
||||
return this.lookPref('theme');
|
||||
},
|
||||
@@ -1605,8 +1611,12 @@ return baseclass.extend({
|
||||
if (ev.key === LOOK_SYNC) {
|
||||
let m = null;
|
||||
try { m = JSON.parse(ev.newValue); } catch (e) {}
|
||||
if (m && Object.prototype.hasOwnProperty.call(LOOK, m.name) && LOOK[m.name].ok.indexOf(m.value) >= 0)
|
||||
if (m && Object.prototype.hasOwnProperty.call(LOOK, m.name) && LOOK[m.name].ok.indexOf(m.value) >= 0) {
|
||||
setLookAttr(this.root, m.name, m.value);
|
||||
/* saved there: a pick that failed here is over */
|
||||
if (this.lookPending) delete this.lookPending[m.name];
|
||||
if (this.lookFailed) delete this.lookFailed[m.name];
|
||||
}
|
||||
}
|
||||
this.applyTheme();
|
||||
this.applyBackdrop();
|
||||
@@ -1641,11 +1651,14 @@ return baseclass.extend({
|
||||
scrolling the page close it. */
|
||||
/* one of Appearance's radio groups */
|
||||
appearGroup(name, legend, opts, set) {
|
||||
const look = { 'vt-colour': 'theme', 'vt-login': 'login', 'vt-backdrop': 'backdrop' }[name];
|
||||
return E('fieldset', { 'class': 'vt-appear-set' }, [
|
||||
E('legend', {}, [ legend ]),
|
||||
E('div', { 'class': 'vt-appear-track' }, opts.map(o => {
|
||||
const input = E('input', { 'type': 'radio', 'name': name, 'value': o[0] });
|
||||
input.addEventListener('change', () => { if (input.checked) set(o[0]); });
|
||||
/* the checked option fires no change: after a failed save, this is the retry */
|
||||
input.addEventListener('click', () => { if (input.checked && (this.lookFailed || {})[look]) set(o[0]); });
|
||||
return E('label', { 'class': 'vt-appear-opt' }, [ input ].concat(o[2] ? [ svg(o[2], 16) ] : [], [ E('span', {}, [ o[1] ]) ]));
|
||||
}))
|
||||
]);
|
||||
@@ -1698,7 +1711,7 @@ return baseclass.extend({
|
||||
const state = this.lookState();
|
||||
const note = state === 'relogin' ? _('Log in again to save Appearance for every browser.')
|
||||
: state !== 'yes' ? _('Saved in this browser only.')
|
||||
: this.lookFailed ? _('Could not save this for every browser. Try again.') : '';
|
||||
: this.lookFailing() ? _('Could not save this for every browser. Try again.') : '';
|
||||
if (this.appearNote) {
|
||||
if (this.appearNote.textContent !== note) this.appearNote.textContent = note;
|
||||
this.appearNote.hidden = !note;
|
||||
|
||||
@@ -70,7 +70,7 @@ msgstr ""
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1664
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1677
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Appearance"
|
||||
@@ -80,7 +80,7 @@ msgstr ""
|
||||
msgid "Authorization Required"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Auto"
|
||||
msgstr ""
|
||||
@@ -150,7 +150,7 @@ msgstr ""
|
||||
msgid "Collapse navigation"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1665
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1678
|
||||
msgid "Colour theme"
|
||||
msgstr ""
|
||||
|
||||
@@ -173,11 +173,11 @@ msgctxt "vantage-theme"
|
||||
msgid "Continues in %s, then comes back here"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1701
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1714
|
||||
msgid "Could not save this for every browser. Try again."
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
@@ -243,7 +243,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Free"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558
|
||||
msgid "Grid packets"
|
||||
msgstr ""
|
||||
|
||||
@@ -293,7 +293,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Leaves unmatched unchanged"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
@@ -303,7 +303,7 @@ msgstr ""
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1699
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1712
|
||||
msgid "Log in again to save Appearance for every browser."
|
||||
msgstr ""
|
||||
|
||||
@@ -313,7 +313,7 @@ msgid "Log out"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1668
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1681
|
||||
msgid "Login background"
|
||||
msgstr ""
|
||||
|
||||
@@ -332,8 +332,8 @@ msgstr ""
|
||||
msgid "Memory"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1948
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1965
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1961
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1978
|
||||
msgctxt "vantage-theme"
|
||||
msgid "More actions"
|
||||
msgstr ""
|
||||
@@ -352,7 +352,7 @@ msgid "Network device table \"%h\""
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1657
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1670
|
||||
msgid "Network path background"
|
||||
msgstr ""
|
||||
|
||||
@@ -375,7 +375,7 @@ msgctxt "vantage-theme"
|
||||
msgid "No rules: %s"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558
|
||||
msgctxt "vantage-theme"
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
@@ -462,7 +462,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Rules table"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1700
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1713
|
||||
msgid "Saved in this browser only."
|
||||
msgstr ""
|
||||
|
||||
@@ -483,11 +483,11 @@ msgid_plural "Show %d more rules"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2105
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2118
|
||||
msgid "Show full description"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2094
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2107
|
||||
msgid "Show full help: %s"
|
||||
msgstr ""
|
||||
|
||||
@@ -495,7 +495,7 @@ msgstr ""
|
||||
msgid "Skip to content"
|
||||
msgstr ""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558
|
||||
msgid "Starfield"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ msgstr "任意数据包"
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1313
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1664
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1677
|
||||
#: luci-theme-vantage/ucode/template/themes/vantage/header.ut:132
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Appearance"
|
||||
@@ -86,7 +86,7 @@ msgstr "外观"
|
||||
msgid "Authorization Required"
|
||||
msgstr "需要授权"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Auto"
|
||||
msgstr "自动"
|
||||
@@ -156,7 +156,7 @@ msgstr "信道"
|
||||
msgid "Collapse navigation"
|
||||
msgstr "收起导航"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1665
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1678
|
||||
msgid "Colour theme"
|
||||
msgstr "颜色主题"
|
||||
|
||||
@@ -179,11 +179,11 @@ msgctxt "vantage-theme"
|
||||
msgid "Continues in %s, then comes back here"
|
||||
msgstr "转入 %s,随后返回此处"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1701
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1714
|
||||
msgid "Could not save this for every browser. Try again."
|
||||
msgstr "无法为所有浏览器保存此设置,请重试。"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Dark"
|
||||
msgstr "深色"
|
||||
@@ -254,7 +254,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Free"
|
||||
msgstr "空闲"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558
|
||||
msgid "Grid packets"
|
||||
msgstr "网格数据包"
|
||||
|
||||
@@ -304,7 +304,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Leaves unmatched unchanged"
|
||||
msgstr "未匹配流量保持原样"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1666
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1679
|
||||
msgctxt "vantage-theme"
|
||||
msgid "Light"
|
||||
msgstr "浅色"
|
||||
@@ -314,7 +314,7 @@ msgstr "浅色"
|
||||
msgid "Log in"
|
||||
msgstr "登录"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1699
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1712
|
||||
msgid "Log in again to save Appearance for every browser."
|
||||
msgstr "重新登录后即可为所有浏览器保存外观设置。"
|
||||
|
||||
@@ -324,7 +324,7 @@ msgid "Log out"
|
||||
msgstr "退出登录"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1316
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1668
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1681
|
||||
msgid "Login background"
|
||||
msgstr "登录背景"
|
||||
|
||||
@@ -343,8 +343,8 @@ msgstr "伪装(NAT)"
|
||||
msgid "Memory"
|
||||
msgstr "内存"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1948
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1965
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1961
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1978
|
||||
msgctxt "vantage-theme"
|
||||
msgid "More actions"
|
||||
msgstr "更多操作"
|
||||
@@ -363,7 +363,7 @@ msgid "Network device table \"%h\""
|
||||
msgstr "网络设备表 \"%h\""
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1320
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1657
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1670
|
||||
msgid "Network path background"
|
||||
msgstr "网络路径背景"
|
||||
|
||||
@@ -386,7 +386,7 @@ msgctxt "vantage-theme"
|
||||
msgid "No rules: %s"
|
||||
msgstr "无规则:%s"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558
|
||||
msgctxt "vantage-theme"
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
@@ -473,7 +473,7 @@ msgctxt "vantage-theme"
|
||||
msgid "Rules table"
|
||||
msgstr "规则表"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1700
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1713
|
||||
msgid "Saved in this browser only."
|
||||
msgstr "仅保存在此浏览器中。"
|
||||
|
||||
@@ -493,11 +493,11 @@ msgid "Show %d more rule"
|
||||
msgid_plural "Show %d more rules"
|
||||
msgstr[0] "再显示 %d 条规则"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2105
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2118
|
||||
msgid "Show full description"
|
||||
msgstr "显示完整描述"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2094
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:2107
|
||||
msgid "Show full help: %s"
|
||||
msgstr "显示完整帮助:%s"
|
||||
|
||||
@@ -505,7 +505,7 @@ msgstr "显示完整帮助:%s"
|
||||
msgid "Skip to content"
|
||||
msgstr "跳至正文"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1552
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/menu-vantage.js:1558
|
||||
msgid "Starfield"
|
||||
msgstr "星空"
|
||||
|
||||
@@ -568,7 +568,9 @@ msgid ""
|
||||
"Vantage theme: save its Appearance (colour mode, login and network path "
|
||||
"backgrounds) and the dashboard's chart style and layouts for every browser, "
|
||||
"in /etc/config/vantage_theme only"
|
||||
msgstr "Vantage 主题:为所有浏览器保存其外观设置(颜色模式、登录页与网络路径背景)以及仪表盘的图表样式和布局,仅写入 /etc/config/vantage_theme"
|
||||
msgstr ""
|
||||
"Vantage 主题:为所有浏览器保存其外观设置(颜色模式、登录页与网络路径背景)以"
|
||||
"及仪表盘的图表样式和布局,仅写入 /etc/config/vantage_theme"
|
||||
|
||||
#: luci-theme-vantage/htdocs/luci-static/resources/vantage-theme/nftflow.js:454
|
||||
msgctxt "vantage-theme"
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||
|
||||
PKG_NAME:=mise
|
||||
PKG_VERSION:=2026.11.2
|
||||
PKG_RELEASE:=51
|
||||
PKG_VERSION:=2026.11.3
|
||||
PKG_RELEASE:=52
|
||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=nexttrace
|
||||
PKG_VERSION:=1.7.5
|
||||
PKG_RELEASE:=18
|
||||
PKG_VERSION:=1.7.6
|
||||
PKG_RELEASE:=19
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+19
-23
@@ -1,51 +1,47 @@
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=pcat-manager
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/photonicat/rockchip_rk3568_pcat_manager.git
|
||||
PKG_SOURCE_DATE:=2023-11-13
|
||||
PKG_SOURCE_DATE:=2023-03-17
|
||||
PKG_SOURCE_URL:=https://github.com/photonicat/rockchip_rk3568_pcat_manager
|
||||
PKG_SOURCE_VERSION:=0e61a4abf35c423420310311f1b2b059deba0cac
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-only
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
PKG_FLAGS:=nonshared
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_INSTALL:=1
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/nls.mk
|
||||
include $(INCLUDE_DIR)/meson.mk
|
||||
|
||||
define Package/pcat-manager
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
TITLE:=PCat System Manager
|
||||
DEPENDS:=@TARGET_rockchip_armv8 +glib2 +libgpiod +libjson-c +libusb-1.0 @BROKEN
|
||||
TITLE:=photonicat watchdog
|
||||
URL:=https://photonicat.com
|
||||
DEPENDS:=+glib2 +libgpiod +libjson-c +libusb-1.0
|
||||
endef
|
||||
|
||||
define Package/pcat-manager/description
|
||||
PCat Manager is a system service for power management
|
||||
and modem controlling.
|
||||
endef
|
||||
|
||||
define Package/pcat-manager/conffiles
|
||||
/etc/pcat-manager-userdata.conf
|
||||
Photonicat Manager is a system service for
|
||||
power management and modem controlling.
|
||||
endef
|
||||
|
||||
define Package/pcat-manager/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/pcat-manager $(1)/usr/sbin/pcat-manager
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_CONF) $(PKG_BUILD_DIR)/conf/pcat-manager.conf.sample $(1)/etc/pcat-manager.conf
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/
|
||||
$(INSTALL_CONF) ./files/pcat-manager.conf $(1)/etc
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/pcat-manager.init $(1)/etc/init.d/pcat-manager
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) \
|
||||
$(PKG_INSTALL_DIR)/usr/bin/pcat-manager \
|
||||
$(1)/usr/bin/pcat-manager
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,pcat-manager))
|
||||
@@ -0,0 +1,28 @@
|
||||
[Hardware]
|
||||
GPIOModemPowerChip=gpiochip4
|
||||
GPIOModemPowerLine=18
|
||||
GPIOModemPowerActiveLow=0
|
||||
GPIOModemRFKillChip=gpiochip4
|
||||
GPIOModemRFKillLine=20
|
||||
GPIOModemRFKillActiveLow=1
|
||||
GPIOModemResetChip=gpiochip0
|
||||
GPIOModemResetLine=8
|
||||
GPIOModemResetActiveLow=0
|
||||
|
||||
[PowerManager]
|
||||
SerialDevice=/dev/ttyS0
|
||||
SerialBaud=115200
|
||||
AutoShutdownVoltageGeneral=0
|
||||
AutoShutdownVoltageLTE=0
|
||||
AutoShutdownVoltage5G=3600
|
||||
LEDHighVoltage=0
|
||||
LEDMediumVoltage=0
|
||||
LEDLowVoltage=0
|
||||
LEDWorkLowVoltage=0
|
||||
StartupVoltage=0
|
||||
ChargerLimitVoltage=0
|
||||
ChargerFastVoltage=0
|
||||
BatteryFullThreshold=4100
|
||||
BatteryDischargeTableNormal=4100;4040;3980;3920;3870;3820;3790;3770;3740;3680;3450
|
||||
BatteryDischargeTable5G=4050;3980;3930;3890;3850;3815;3780;3740;3700;3655;3610
|
||||
BatteryChargeTable=4100;4085;4067;4040;4003;3942;3880;3825;3780;3725;3600
|
||||
@@ -1,12 +1,24 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=45
|
||||
STOP=89
|
||||
|
||||
start_service() {
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/sbin/pcat-manager
|
||||
procd_set_param respawn 3600 5 0
|
||||
procd_close_instance
|
||||
start() {
|
||||
service_start /usr/bin/pcat-manager -D
|
||||
}
|
||||
|
||||
stop() {
|
||||
service_stop /usr/bin/pcat-manager
|
||||
}
|
||||
|
||||
restart() {
|
||||
killall -USR1 pcat-manager
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
touch /tmp/pcat-manager-shutdown.tmp
|
||||
stop
|
||||
rfkill block wwan 2>/dev/null || true
|
||||
}
|
||||
Reference in new issue
Block a user