🏅 Sync 2026-11-07 00:22:58

This commit is contained in:
github-actions[bot] committed 2026-11-07 00:22:58 +08:00
1 parent 47b676665c
commit 0810753c5c
50 files changed
+1588 -1305

No files matched your search

+1 -1
View File
@@ -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
+30 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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)/
+1 -1
View File
@@ -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') ])
]));
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+19 -1
View File
@@ -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 기능을 출시하였습니다. 의심 징후를 탐지하고 감지하고 알립니다.
### 개선
+2 -2
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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"
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 ""
+20 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
+28
View File
@@ -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
+18 -6
View File
@@ -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
}