diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index a978740af..575fdfc7f 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all PKG_VERSION:=0.1.51 -PKG_RELEASE:=59 +PKG_RELEASE:=60 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/README.md b/luci-app-fwlive/README.md index e723d6fbc..dc0c40d13 100644 --- a/luci-app-fwlive/README.md +++ b/luci-app-fwlive/README.md @@ -29,7 +29,7 @@ LuCI **Firewall Live View** — client-side JS view polling `ubus fwlive poll` ( | `root/usr/libexec/fwlive-adaptive-cap.sh` | Layer 1 adaptive poll cap (sourced by `rpcd/fwlive`) | | `root/usr/libexec/fwlive-logging.sh` | WAN zone logging helpers | | `root/lib/upgrade/keep.d/luci-app-fwlive` | Keep `/etc/fwlive/` across sysupgrade | -| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging** (and best-effort on already-on enable); `prerm` restores it on uninstall. Restore is skipped (file kept; `fwlive` syslog only) when there is no WAN zone, the lock is unavailable, firewall changes are pending, the commit does not verify, reload fails, or the value after reload does not match the baseline. Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. | +| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging** (and best-effort on already-on enable); `prerm` restores it on uninstall. A successful disable/reload, including an already-disabled retry, drops the marker when no later fwlive toggle has taken ownership; this preserves operator changes to other log-mask bits, including staged edits. If logging is already off but the marker remains, Disable retries `fw4 reload`; pending UCI changes return `firewall_changes_pending` without reloading, and reload failure returns `firewall_reload_failed` while retaining the marker. Lock, read, or generation mismatch also keeps it for uninstall recovery. Restore is skipped (file kept; `fwlive` syslog only) when there is no WAN zone, the lock is unavailable, firewall changes are pending, the commit does not verify, reload fails, or the value after reload does not match the baseline. Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. | | `root/usr/libexec/fwlive-log-filter.sh` | Server-side firewall-only filter (`isFirewallEvent` parity) | | `root/usr/libexec/fwlive-is-firewall-event.sh` | Shared filter logic (sourced by filter + tests) | | `root/usr/libexec/fwlive-is-firewall-event.awk` | **Generated** standalone classifier from `CLASSIFY_SPEC`; loaded by `fwlive-is-firewall-event.sh` | diff --git a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh index 5f6e8453c..29bf89305 100755 --- a/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh +++ b/luci-app-fwlive/root/usr/libexec/fwlive-logging.sh @@ -1029,6 +1029,12 @@ commit_wan_log_change() { report_wan_log_after_commit() { _rc="$1" zone_json="$2" + _drop_baseline=0 + # The optional eighth argument opts in only when its value is exactly 1. + # Argument count alone must never authorize deleting recovery state. + if [ "${8:-0}" = 1 ]; then + _drop_baseline=1 + fi if [ "$_rc" -eq 1 ]; then return 0 fi @@ -1038,11 +1044,16 @@ report_wan_log_after_commit() { wan_log_error_json "$zone_json" firewall_reload_failed return 0 fi + if [ "$_drop_baseline" -eq 1 ]; then + drop_wan_log_baseline_after_disable "$zone" "$WAN_LOG_COMMIT_GENERATION" + fi wan_log_error_json "$zone_json" firewall_commit_raced return 0 fi - shift 2 - reload_and_report_wan_log "$@" "$zone_json" "$WAN_LOG_COMMIT_GENERATION" + # Forward only the five documented operation fields plus an explicit flag; + # unrelated trailing caller arguments must not shift into the flag position. + reload_and_report_wan_log "$3" "$4" "$5" "$6" "$7" \ + "$zone_json" "$WAN_LOG_COMMIT_GENERATION" "$_drop_baseline" } # Firewall reload + best-effort UCI rollback on reload failure. The reload @@ -1065,6 +1076,10 @@ reload_and_report_wan_log() { success_msg="$5" zone_json="$6" committed_generation="$7" + _drop_baseline=0 + if [ "${8:-0}" = 1 ]; then + _drop_baseline=1 + fi if ! reload_firewall; then # Re-acquire the logging lock so the rollback decision is atomic @@ -1097,11 +1112,43 @@ reload_and_report_wan_log() { wan_log_error_json "$zone_json" firewall_reload_failed return 0 fi + if [ "$_drop_baseline" -eq 1 ]; then + drop_wan_log_baseline_after_disable "$zone" "$committed_generation" + fi logger -t fwlive "$success_msg" 2>/dev/null || true printf '{"ok":true,"changed":true,"wan_zone":%s}' "$zone_json" return 0 } +# After a successful disable/reload, discard the uninstall marker unless a +# later fwlive toggle has taken ownership again. Pending external UCI edits +# are preserved by retiring the stale marker too; a valid `uci show` confirms +# the config is readable before unlinking it. +drop_wan_log_baseline_after_disable() { + drop_zone="$1" + drop_generation="$2" + _drop_path="$(wan_log_baseline_path)" + [ -f "$_drop_path" ] && [ ! -L "$_drop_path" ] || return 0 + if ! acquire_wan_log_lock; then + return 0 + fi + if ! uci -q show "firewall.${drop_zone}" >/dev/null 2>&1; then + release_wan_log_lock + return 0 + fi + _drop_current_generation=$(wan_log_generation_read 2>/dev/null) || { + release_wan_log_lock + return 0 + } + if [ -z "$drop_generation" ] || [ "$drop_generation" = unavailable ] \ + || [ "$_drop_current_generation" != "$drop_generation" ]; then + release_wan_log_lock + return 0 + fi + rm -f "$_drop_path" 2>/dev/null || true + release_wan_log_lock +} + # $1=enable checks nf_log before the lock. Success leaves the lock held # and zone / zone_json set. Failure prints JSON and returns non-zero. wan_log_toggle_preamble() { @@ -1182,12 +1229,27 @@ disable_wan_logging() { current=$(wan_zone_log_value "$zone") if [ -z "$current" ] || ! wan_filter_log_enabled "$current"; then - if ! wan_log_generation_bump >/dev/null; then + _disable_generation=$(wan_log_generation_bump) || { release_wan_log_lock wan_log_tracking_failed_json "$zone_json" return 0 + } + _baseline_path="$(wan_log_baseline_path)" + if [ -f "$_baseline_path" ] && [ ! -L "$_baseline_path" ] \ + && firewall_changes_pending; then + release_wan_log_lock + wan_log_error_json "$zone_json" firewall_changes_pending + return 0 fi release_wan_log_lock + if [ -f "$_baseline_path" ] && [ ! -L "$_baseline_path" ]; then + if ! reload_firewall; then + logger -t fwlive "Firewall reload failed after already-disabled request" 2>/dev/null || true + wan_log_error_json "$zone_json" firewall_reload_failed + return 0 + fi + drop_wan_log_baseline_after_disable "$zone" "$_disable_generation" + fi printf '{"ok":true,"changed":false,"wan_zone":%s}' "$zone_json" return 0 fi @@ -1203,7 +1265,7 @@ disable_wan_logging() { report_wan_log_after_commit "$_commit_rc" "$zone_json" \ "$zone" "$current" "$target" \ 'Firewall reload failed after disable; reverted UCI WAN log' \ - 'WAN zone logging disabled' + 'WAN zone logging disabled' 1 return 0 } diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 13de9dea2..b4a817d8c 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -7,8 +7,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=26.10.3 -PKG_RELEASE:=312 +PKG_VERSION:=26.10.4 +PKG_RELEASE:=313 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua index 73926d3fc..d52f6037e 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua @@ -429,6 +429,16 @@ if singbox_tags:find("with_quic") then o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) o:depends({ protocol = "hysteria2" }) + o = s:option(Value, "hysteria2_stream_recv_win", translate("QUIC stream receive window")) + o.datatype = "uinteger" + o.placeholder = "8388608" + o:depends({ protocol = "hysteria2" }) + + o = s:option(Value, "hysteria2_conn_recv_win", translate("QUIC connection receive window")) + o.datatype = "uinteger" + o.placeholder = "20971520" + o:depends({ protocol = "hysteria2" }) + o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") o.datatype = "range(4,120)" o:depends({ protocol = "hysteria2"}) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua index 24ac84e95..2b3163c07 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua @@ -361,6 +361,16 @@ o:depends({ protocol = "hysteria2" }) o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) o:depends({ protocol = "hysteria2" }) +o = s:option(Value, "hysteria2_stream_recv_win", translate("QUIC stream receive window")) +o.datatype = "uinteger" +o.placeholder = "8388608" +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "hysteria2_conn_recv_win", translate("QUIC connection receive window")) +o.datatype = "uinteger" +o.placeholder = "20971520" +o:depends({ protocol = "hysteria2" }) + o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") o.datatype = "range(4,120)" o:depends({ protocol = "hysteria2"}) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/5_hysteria2.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/5_hysteria2.lua index bd9a4130b..e57fedfe3 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/5_hysteria2.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/5_hysteria2.lua @@ -109,10 +109,14 @@ o.rewrite_option = o.option o = s:option(Value, "down_mbps", translate("Max download Mbps")) o.rewrite_option = o.option -o = s:option(Value, "recv_window", translate("QUIC stream receive window")) +o = s:option(Value, "stream_recv_win", translate("QUIC stream receive window")) +o.datatype = "uinteger" +o.placeholder = "8388608" o.rewrite_option = o.option -o = s:option(Value, "recv_window_conn", translate("QUIC connection receive window")) +o = s:option(Value, "conn_recv_win", translate("QUIC connection receive window")) +o.datatype = "uinteger" +o.placeholder = "20971520" o.rewrite_option = o.option o = s:option(Value, "idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") diff --git a/luci-app-passwall/luasrc/passwall/util_hysteria2.lua b/luci-app-passwall/luasrc/passwall/util_hysteria2.lua index 21c0954fa..110e58fbd 100644 --- a/luci-app-passwall/luasrc/passwall/util_hysteria2.lua +++ b/luci-app-passwall/luasrc/passwall/util_hysteria2.lua @@ -144,8 +144,8 @@ function gen_config(var) ech = (node.ech == "1") and node.ech_config or nil }, quic = { - initStreamReceiveWindow = (node.hysteria2_recv_window) and tonumber(node.hysteria2_recv_window) or nil, - initConnReceiveWindow = (node.hysteria2_recv_window_conn) and tonumber(node.hysteria2_recv_window_conn) or nil, + initStreamReceiveWindow = node.hysteria2_stream_recv_win and tonumber(node.hysteria2_stream_recv_win) or nil, + initConnReceiveWindow = node.hysteria2_conn_recv_win and tonumber(node.hysteria2_conn_recv_win) or nil, maxIdleTimeout = (function(t) t = tonumber(tostring(t or "30"):match("^%d+")) return (t and t >= 4 and t <= 120) and t .. "s" or "30s" diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index a7de4ffe6..9a09e4758 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -623,7 +623,9 @@ function gen_outbound(flag, node, tag, proxy_table) t = tonumber(tostring(t or "0"):match("^%d+")) return (t and t >= 2 and t <= 60) and t .. "s" or nil end)(node.hysteria2_keep_alive_period), - disable_path_mtu_discovery = version_ge_1_14_0 and (tonumber(node.hysteria2_disable_mtu_discovery) == 1) or nil, + stream_receive_window = tonumber(node.hysteria2_stream_recv_win) and tonumber(node.hysteria2_stream_recv_win)/1048576 .. " MB" or nil, + connection_receive_window = tonumber(node.hysteria2_conn_recv_win) and tonumber(node.hysteria2_conn_recv_win)/1048576 .. " MB" or nil, + disable_path_mtu_discovery = version_ge_1_14_0 and (node.hysteria2_disable_mtu_discovery == "1") or nil, tls = tls, realm = node.hysteria2_realms and (function() result.server = nil diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index ce1f57001..4e7b7de4d 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -364,7 +364,9 @@ function gen_outbound(flag, node, tag, proxy_table) t = tonumber(tostring(t or "0"):match("^%d+")) return (t and t >= 2 and t <= 60) and t or nil end)(node.hysteria2_keep_alive_period), - disablePathMTUDiscovery = tonumber(node.hysteria2_disable_mtu_discovery) == 1 + disablePathMTUDiscovery = node.hysteria2_disable_mtu_discovery == "1", + initStreamReceiveWindow = node.hysteria2_stream_recv_win and tonumber(node.hysteria2_stream_recv_win) or nil, + initConnectionReceiveWindow = node.hysteria2_conn_recv_win and tonumber(node.hysteria2_conn_recv_win) or nil, } end if fragment and fragment_table and ({raw=1, ws=1, httpupgrade=1, grpc=1, xhttp=1})[TP] then diff --git a/luci-mod-status/Makefile b/luci-mod-status/Makefile index 96eb1e31e..e32126619 100644 --- a/luci-mod-status/Makefile +++ b/luci-mod-status/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Status Pages LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket -PKG_RELEASE:=25 +PKG_RELEASE:=26 PKG_BUILD_DEPENDS:=iwinfo PKG_LICENSE:=Apache-2.0 diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js b/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js index 296db76d4..156870626 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/channel_analysis.js @@ -28,7 +28,7 @@ function parseIwDevMap(stdout) { return Object.entries(byPhy).reduce((map, entry) => { const phy = entry[0]; const ifaces = entry[1]; - const preferred = ifaces.find((ifname) => !/^wifi\d+$/.test(ifname)) || ifaces[0]; + const preferred = ifaces.find((ifname) => !/^(wifi\d+$|tmpsta\d+$|tmp\.)/.test(ifname)) || ifaces.find((ifname) => /^wifi\d+$/.test(ifname)) || ifaces[0]; const temp = `tmpsta${phy.replace(/^phy/, '')}`; ifaces.forEach((ifname) => { @@ -374,7 +374,19 @@ return view.extend({ .finally(L.bind(this.cleanupTemporaryScanIface, this, tempIfname)); }, + isQcaRadio(radioDev) { + return ['qcawifi', 'qcawificfg80211'].includes(radioDev.get('type')); + }, + resolveScanDevice(radioDev) { + if (this.isQcaRadio(radioDev)) { + // QCA netifd may expose logical wifiN.networkM names instead of VAPs. + const radio = this.iwDevMap?.[radioDev.getName()]; + const ifname = radio?.preferred; + if (!ifname || ifname == radioDev.getName()) + return Promise.reject(new Error(_('No active wireless interface found for %s').format(radioDev.getName()))); + return Promise.resolve(ifname); + } return radioDev.getWifiNetworks().then((networks) => { const ap = networks.find(net => net.getMode() == 'ap') || networks[0]; return ap?.getIfname() || radioDev.getName(); @@ -386,6 +398,15 @@ return view.extend({ return radioDev.getScanList(); return this.resolveScanDevice(radioDev).then((scanIfname) => { + if (this.isQcaRadio(radioDev)) { + // QCA STA teardown also stops AP VAPs through the repeater state machine. + // Scan the existing VAP without changing the radio topology. + return fs.exec('/usr/sbin/iw', [ 'dev', scanIfname, 'scan', 'ap-force' ]).then((res) => { + if (res.code !== 0 || /scan aborted!/i.test(res.stdout || '')) + throw new Error(res.stderr?.trim() || res.stdout?.trim() || _('Wireless scan failed')); + return parseIwScan(res.stdout); + }); + } const iwDev = this.iwDevMap?.[scanIfname] || this.iwDevMap?.[radioDev.getName()]; const scanTasks = []; @@ -793,24 +814,19 @@ return view.extend({ return Promise.all([ this.loadSVG(L.resource('svg/channel_analysis.svg')), L.resolveDefault(fs.exec('/usr/sbin/iw', [ 'dev' ]), null), - network.getWifiDevices().then(L.bind(function(data) { - const tasks = [], ret = []; + network.getWifiDevices() + ]).then(([svg, iwDevs, devices]) => { + this.iwDevMap = parseIwDevMap(iwDevs?.stdout); + const ret = {}; - for (let d of data) { - ret[d.getName()] = { dev : d }; - - tasks.push(this.callFrequencyList(d.getName()) - .then(L.bind(function(radio, data) { - ret[radio.getName()].freq = data; - }, this, d))); - } - - return Promise.all(tasks).then(function() { return ret; }) - }, this)) - ]).then((data) => { - this.iwDevMap = parseIwDevMap(data[1]?.stdout); - - return [ data[0], data[2] ]; + return Promise.all(devices.map((dev) => { + const name = dev.getName(); + // Query an existing QCA VAP; querying wifiN creates a temporary VAP. + const target = this.isQcaRadio(dev) ? this.iwDevMap[name]?.preferred : name; + const freqs = target && (!this.isQcaRadio(dev) || target != name) + ? this.callFrequencyList(target) : Promise.resolve([]); + return freqs.then((freq) => { ret[name] = { dev, freq }; }); + })).then(() => [svg, ret]); }); }, diff --git a/luci-mod-status/tests/channel-analysis.js b/luci-mod-status/tests/channel-analysis.js index 0b1f440fb..810a972de 100644 --- a/luci-mod-status/tests/channel-analysis.js +++ b/luci-mod-status/tests/channel-analysis.js @@ -2,9 +2,15 @@ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const source = fs.readFileSync(path.join(__dirname, '../htdocs/luci-static/resources/view/status/channel_analysis.js'), 'utf8'); -const view = new Function('view', 'rpc', 'L', 'fs', source)( +const execCalls = []; +let devices = []; +const frequencies = []; +let execResult = {code: 0, stdout: ''}; +const view = new Function('view', 'rpc', 'L', 'fs', 'network', source)( {extend: o => o}, {declare: () => () => {}}, - {resolveDefault: (p, fallback) => p.catch(() => fallback)}, {} + {resolveDefault: (p, fallback) => p.catch(() => fallback), resource: p => p}, + {exec: async (cmd, args) => {execCalls.push([cmd, args]); return execResult;}}, + {getWifiDevices: async () => devices} ); view.updateScanButton = () => {}; const dev = (name, type) => ({getName: () => name, get: () => type}); @@ -12,9 +18,35 @@ const dev = (name, type) => ({getName: () => name, get: () => type}); const mt = dev('ra', 'mt_dbdc'); mt.getScanList = async () => [{ssid:'mtwifi'}]; assert.deepEqual(await view.getScanResultsForRadio(mt), [{ssid:'mtwifi'}]); + String.prototype.format = function(...args) {let i=0;return this.replace(/%s/g,()=>args[i++]);}; + global._ = s => s; const qca = dev('wifi0', 'qcawificfg80211'); - qca.getWifiNetworks = async () => [{getMode:()=> 'ap',getIfname:()=> 'ath0'}]; + view.iwDevMap = {wifi0: {phy:'phy0', preferred:'ath0', temp:'tmpsta0'}}; + qca.getWifiNetworks = async () => [{getMode:()=> 'ap',getIfname:()=> 'wifi0.network1'}]; assert.equal(await view.resolveScanDevice(qca), 'ath0'); + assert.deepEqual(await view.getScanResultsForRadio(qca), []); + assert.deepEqual(execCalls, [['/usr/sbin/iw', ['dev','ath0','scan','ap-force']]], 'QCA scans a real VAP without creating a temporary STA'); + execResult = {code:237, stderr:'No such device'}; + await assert.rejects(view.getScanResultsForRadio(qca), /No such device/); + execResult = {code:0, stdout:'scan aborted!'}; + await assert.rejects(view.getScanResultsForRadio(qca), /scan aborted/); + await assert.rejects(view.resolveScanDevice(dev('wifi2', 'qcawifi')), /No active wireless interface/); + execResult = {code:0, stdout:'BSS 00:11:22:33:44:55(on ath0)\n\tfreq: 2412\n\tsignal: -42.00 dBm\n\tSSID: test\n'}; + const scan = await view.getScanResultsForRadio(qca); + assert.equal(scan[0].band,2); + assert.equal(scan[0].signal,-42); + + // Frequency queries must also use real VAPs, including a third QCA radio. + devices = [qca,dev('wifi2','qcawifi'),dev('radio0','mac80211')]; + execResult = {code:0,stdout:'phy#7\n\tInterface tmpsta7\n\tInterface ath2\n\tInterface wifi2\nphy#0\n\tInterface ath0\n\tInterface wifi0\n'}; + view.loadSVG = async () => 'svg'; + view.callFrequencyList = async name => {frequencies.push(name);return [];}; + await view.load(); + assert.deepEqual(frequencies, ['ath0','ath2','radio0']); + assert.equal(await view.resolveScanDevice(dev('wifi2','qcawifi')), 'ath2'); + const generic=dev('radio0','mac80211'); + generic.getWifiNetworks=async()=>[{getMode:()=> 'ap',getIfname:()=> 'wlan0'}]; + assert.equal(await view.resolveScanDevice(generic),'wlan0'); let calls = [], release; view.getScanResultsForRadio = d => { calls.push(d.getName()); diff --git a/luci-mod-status/ucode/template/admin_status/index.ut b/luci-mod-status/ucode/template/admin_status/index.ut index dafb427bf..981265936 100644 --- a/luci-mod-status/ucode/template/admin_status/index.ut +++ b/luci-mod-status/ucode/template/admin_status/index.ut @@ -6,8 +6,6 @@ {% include('header') %} -

{{ _('Status') }}

-
{{ _('Loading view…') }}
diff --git a/msd_lite/Makefile b/msd_lite/Makefile index 6846eb0d6..1b9f00489 100644 --- a/msd_lite/Makefile +++ b/msd_lite/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=msd_lite -PKG_RELEASE:=4 +PKG_RELEASE:=5 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/rozhuk-im/msd_lite.git PKG_SOURCE_DATE:=2026-07-21 -PKG_SOURCE_VERSION:=null +PKG_SOURCE_VERSION:=fa68e131343fb58c67ad77b2d26f2cb7c49a2c95 PKG_MIRROR_HASH:=skip PKG_MAINTAINER:=Tianling Shen