diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile
index c3aeb0624..f8f12e975 100644
--- a/luci-app-passwall2/Makefile
+++ b/luci-app-passwall2/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.14
-PKG_RELEASE:=84
+PKG_RELEASE:=85
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_xray.lua
similarity index 100%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_xray.lua
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_sing-box.lua
similarity index 100%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/sing-box.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_sing-box.lua
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/3_hysteria2.lua
similarity index 100%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/hysteria2.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/3_hysteria2.lua
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/4_ss-rust.lua
similarity index 100%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ss-rust.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/4_ss-rust.lua
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/5_ssr.lua
similarity index 100%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ssr.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/5_ssr.lua
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/6_naive.lua
similarity index 100%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/naive.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/6_naive.lua
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_xray.lua
similarity index 95%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_xray.lua
index ae79afb33..48107b1b3 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ray.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_xray.lua
@@ -11,6 +11,10 @@ if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
+if not s1.val["type"] then
+ s1.val["type"] = type_name
+end
+
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -29,6 +33,27 @@ local header_type_list = {
o = s:option(Flag, "custom", translate("Use Custom Config"))
+o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
+o.rows = 10
+o.wrap = "off"
+o:depends({ custom = true })
+o.datatype = "json"
+local o_validate = o.validate
+o.validate = function(self, value)
+ local v = o_validate(self, value)
+ if v then return v end
+ return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
+end
+o.custom_cfgvalue = function(self, section, value)
+ local config_str = m:get(section, "config_str")
+ if config_str then
+ return api.base64Decode(config_str)
+ end
+end
+o.custom_write = function(self, section, value)
+ m:set(section, "config_str", api.base64Encode(value) or "")
+end
+
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("vmess", "Vmess")
o:value("vless", "VLESS")
@@ -41,6 +66,12 @@ o:value("wireguard", "WireGuard")
o:value("dokodemo-door", "dokodemo-door")
o:depends({ custom = false })
+o = s:option(DummyValue, "is_endpoint", "")
+o.not_rewrite = true
+o.template = m:template_path("/cbi/hidevalue")
+o.value = "1"
+o:depends({ custom = false, protocol = "wireguard" })
+
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
@@ -385,7 +416,7 @@ end
--[[acceptProxyProtocol]]
o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
o.default = "0"
-o:depends({ custom = false })
+o:depends({ custom = false, is_endpoint = "" })
--[[Fast Open]]
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
@@ -444,8 +475,7 @@ o.datatype = "base64"
o:depends({ protocol = "wireguard" })
o = s:option(DummyValue, "gen_wireguard_key")
-o.prefix = option_prefix
-o.template = m:template_path("/server/server_wireguard")
+o.template = m:template_path("/server/gen_wireguard_key")
o:depends({ protocol = "wireguard" })
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
@@ -505,27 +535,6 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
-o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
-o.rows = 10
-o.wrap = "off"
-o:depends({ custom = true })
-o.datatype = "json"
-local o_validate = o.validate
-o.validate = function(self, value)
- local v = o_validate(self, value)
- if v then return v end
- return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
-end
-o.custom_cfgvalue = function(self, section, value)
- local config_str = m:get(section, "config_str")
- if config_str then
- return api.base64Decode(config_str)
- end
-end
-o.custom_write = function(self, section, value)
- m:set(section, "config_str", api.base64Encode(value) or "")
-end
-
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_sing-box.lua
similarity index 94%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_sing-box.lua
index 59b772e34..0d5a713bd 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/sing-box.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_sing-box.lua
@@ -13,6 +13,10 @@ if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
+if not s1.val["type"] then
+ s1.val["type"] = type_name
+end
+
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -30,6 +34,27 @@ local ss_method_list = {
o = s:option(Flag, "custom", translate("Use Custom Config"))
+o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
+o.rows = 10
+o.wrap = "off"
+o:depends({ custom = true })
+o.datatype = "json"
+local o_validate = o.validate
+o.validate = function(self, value)
+ local v = o_validate(self, value)
+ if v then return v end
+ return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
+end
+o.custom_cfgvalue = function(self, section, value)
+ local config_str = m:get(section, "config_str")
+ if config_str then
+ return api.base64Decode(config_str)
+ end
+end
+o.custom_write = function(self, section, value)
+ m:set(section, "config_str", api.base64Encode(value) or "")
+end
+
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("mixed", "Mixed")
o:value("socks", "Socks")
@@ -55,6 +80,12 @@ end
o:value("direct", "Direct")
o:depends({ custom = false })
+o = s:option(DummyValue, "is_endpoint", "")
+o.not_rewrite = true
+o.template = m:template_path("/cbi/hidevalue")
+o.value = "1"
+o:depends({ custom = false, protocol = "wireguard" })
+
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
@@ -420,14 +451,13 @@ if singbox_tags:find("with_wireguard") then
o:depends({ protocol = "wireguard" })
o = s:option(DummyValue, "gen_wireguard_key")
- o.prefix = option_prefix
- o.template = m:template_path("/server/server_wireguard")
+ o.template = m:template_path("/server/gen_wireguard_key")
o:depends({ protocol = "wireguard" })
end
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
-o:depends({ custom = false })
+o:depends({ custom = false, is_endpoint = "" })
o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
o.default = "0"
@@ -482,27 +512,6 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
-o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
-o.rows = 10
-o.wrap = "off"
-o:depends({ custom = true })
-o.datatype = "json"
-local o_validate = o.validate
-o.validate = function(self, value)
- local v = o_validate(self, value)
- if v then return v end
- return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
-end
-o.custom_cfgvalue = function(self, section, value)
- local config_str = m:get(section, "config_str")
- if config_str then
- return api.base64Decode(config_str)
- end
-end
-o.custom_write = function(self, section, value)
- m:set(section, "config_str", api.base64Encode(value) or "")
-end
-
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_hysteria2.lua
similarity index 95%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_hysteria2.lua
index 8c6114e8c..bd6c5c1de 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/hysteria2.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_hysteria2.lua
@@ -8,6 +8,10 @@ local type_name = "Hysteria2"
s1.fields["type"]:value(type_name, "Hysteria2")
+if not s1.val["type"] then
+ s1.val["type"] = type_name
+end
+
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ss-rust.lua
similarity index 93%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ss-rust.lua
index 1c7e88c0e..80cb56c7b 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ss-rust.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ss-rust.lua
@@ -8,6 +8,10 @@ local type_name = "SS-Rust"
s1.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
+if not s1.val["type"] then
+ s1.val["type"] = type_name
+end
+
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/5_ssr.lua
similarity index 94%
rename from luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua
rename to luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/5_ssr.lua
index 32494fb72..958c98124 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/ssr.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/5_ssr.lua
@@ -8,6 +8,10 @@ local type_name = "SSR"
s1.fields["type"]:value(type_name, translate("ShadowsocksR"))
+if not s1.val["type"] then
+ s1.val["type"] = type_name
+end
+
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua
index 533824f6b..779bce87f 100644
--- a/luci-app-passwall2/luasrc/passwall2/api.lua
+++ b/luci-app-passwall2/luasrc/passwall2/api.lua
@@ -1554,6 +1554,7 @@ function luci_types(s, s2)
for i, v in ipairs(s2.children) do
local o = s2.children[i]
o.config_option = o.option
+ o.option_prefix = option_prefix
o.option = option_prefix .. o.option
if not o.not_rewrite then
o.cfgvalue = function(self, section)
diff --git a/luci-app-passwall2/luasrc/view/passwall2/server/server_wireguard.htm b/luci-app-passwall2/luasrc/view/passwall2/server/gen_wireguard_key.htm
similarity index 96%
rename from luci-app-passwall2/luasrc/view/passwall2/server/server_wireguard.htm
rename to luci-app-passwall2/luasrc/view/passwall2/server/gen_wireguard_key.htm
index ff73bb97c..1283e6f7d 100644
--- a/luci-app-passwall2/luasrc/view/passwall2/server/server_wireguard.htm
+++ b/luci-app-passwall2/luasrc/view/passwall2/server/gen_wireguard_key.htm
@@ -1,7 +1,7 @@
<%
local map = self.map
local api = map.api
-local prefix = self.prefix
+local prefix = self.option_prefix or ""
-%>
" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
index c4f657b8e..70bb852fb 100755
--- a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
+++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua
@@ -535,6 +535,7 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
result = set_ss_implementation(sub_ss_type, result)
if not result then return nil end
result.method = node.cipher
+ result.ss_method = node.cipher
result.password = node.password
if node.plugin == "obfs" then
result.plugin = "obfs-local"
@@ -1114,6 +1115,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
(_method == "xchacha20-poly1305" and "xchacha20-ietf-poly1305") or _method
result.method = method
+ result.ss_method = method
result.password = password
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
@@ -1482,6 +1484,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.port = content.port
result.password = content.password
result.method = content.encryption
+ result.ss_method = content.encryption
result.plugin = content.plugin
result.plugin_opts = content.plugin_options
result.group = content.airport
@@ -1770,6 +1773,13 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
elseif has_hysteria2 then
result.type = "Hysteria2"
+ for k1, v1 in pairs(result) do
+ if k1:find("hysteria2_") == 1 then
+ local s = split(k1, "hysteria2_")
+ result[s[2]] = v1
+ result[k1] = nil
+ end
+ end
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "Hysteria2", "Hysteria2"))
return nil
diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile
index 233d4a5ef..847051475 100644
--- a/luci-theme-footstrap/Makefile
+++ b/luci-theme-footstrap/Makefile
@@ -17,12 +17,12 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
-PKG_RELEASE:=19
+PKG_RELEASE:=20
endif
LUCI_TITLE:=Footstrap Theme
# +luci-base is the WHOLE list, and keeping it that way is the constraint. Nothing this package
-# ships fetches anything at run time any more (the self-updater is retired), so there is no second
+# ships fetches anything at run time (the self-update script is retired), so there is no second
# name to justify — and the rule that got it here stands: the one convenience tool that ever wanted
# a dep was `curl`, which is NOT in OpenWrt's default set, on an 8 MB device, for one API call. Fall
# back to what the base image has (uclient-fetch, jsonfilter, sha256sum) or do without.
@@ -168,8 +168,8 @@ endef
# what Weblate can see (it translates `po/`, and only `po/`).
#
# It was `i18n/` here for a while, with a po2lmo loop in this hook bundling the catalogue into the
-# theme package, because a multi-package release was mis-picked by a self-updater that resolved the
-# theme by name and took `head -1` (issue #6). That updater is retired and the release is built by
+# theme package, because a multi-package release was mis-picked by a self-update script that resolved
+# the theme by name and took `head -1` (issue #6). That script is retired and the release is built by
# owfeed, which packages this theme as exactly one artifact per format regardless of what luci.mk
# would do — so the constraint that bought the rename is gone, and the cost of keeping it (a
# catalogue invisible to the project's own translation platform) is not.
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
index 8d08c7952..906dee699 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
@@ -4,6 +4,7 @@
'require fs-fit as fit';
'require fs-prefs as prefs';
'require fs-menutree as tree';
+'require fs-version as version';
/* The chrome AROUND the content: the mode menu, the section tabs, the rail toggle, and the
* measurements that decide how much room any of it gets. The MAIN menu is not here — it is injected
@@ -277,6 +278,40 @@ function publishBarHeight(bar) {
root.style.setProperty('--fs-bar-live', `${h}px`);
}
+/* ---- the theme names itself in the footer, beside LuCI and the distribution ----
+ *
+ * The footer's sentence is about what renders this page: LuCI's version, the distribution's, and —
+ * since the markup is the theme's — the theme's. Each of the other two is a link to where that
+ * software lives, so this one is too.
+ *
+ * WRITTEN FROM THE CLIENT, and that is the only reason it is here rather than in footer.ut: the
+ * version is stamped into fs-version.js by file name — the Makefile, tools/stage.sh and dev-sync.sh
+ * all sed that one literal — so a second copy in a template would be a second stamp site to keep in
+ * step, and a footer that quietly disagrees with the Appearance tab is worse than no version at all.
+ * The label is the one fs-version already prints there, dev builds included.
+ *
+ * Once per document: the footer survives a client navigation (navigate() sweeps the children of
+ * .fs-content, and the footer is its sibling), and the guard makes a second call a no-op rather
+ * than a second copy. No request, no translated string: a proper noun and a number. */
+function nameThemeInFooter() {
+ const host = document.querySelector('footer.fs-footer span');
+ if (!host || host.querySelector('.fs-footer-self')) return;
+ const self = E('a', {
+ class: 'fs-footer-self',
+ href: version.REPO_URL,
+ target: '_blank',
+ rel: 'noreferrer',
+ }, version.label());
+ /* AFTER THE DISTRIBUTION, not at the end. The sentence lists the software that renders the page
+ * — LuCI, the distribution, the theme — and footer.ut appends one more fragment after them that
+ * is not a component but a state: "Lua compatibility mode active". Landing behind it read as if
+ * the theme were part of that notice. Two links means the second is the distribution's; with
+ * anything else there, appending is still correct. */
+ const links = host.querySelectorAll('a');
+ if (links.length >= 2) links[1].after(document.createTextNode(' / '), self);
+ else host.append(document.createTextNode(' / '), self);
+}
+
/* ---- does the right-hand cluster still fit beside the brand? ----
*
* The same question as the menu's, one row up, and it cannot be asked the same way. The cluster is
@@ -459,6 +494,7 @@ function wireIndicatorCounts() {
return baseclass.extend({
setRenderMain,
renderChrome,
+ nameThemeInFooter,
wireIndicatorCounts,
/* registered with fs-fit by the theme's init(): the bar's "does the menu fit beside the brand"
* measurement rides the same engine as the data tables' */
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js
index 9c4eb768f..f733e937a 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-router.js
@@ -125,10 +125,14 @@ function warnPollUnreadable(msg) {
* itself to L's prototype when the FIRST requirer compiles it, so this sees the instance the pages
* actually use, and a router that required it would both bind it to its own prototypal L (the two-L
* trap, docs/spa-router.md) and pull uci.js onto pages that never touch uci. No instance means no
- * cache to flush. */
+ * cache to flush.
+ *
+ * Returns the refill below as a promise the caller must wait on, or null when there is nothing to
+ * wait for. It never rejects — a failed refill is reported and swallowed, because a navigation is
+ * not the place to lose a page over a config the incoming view may not even read. */
function flushUciCache() {
const uci = window.L ? window.L.uci : null;
- if (!uci || typeof uci.unload !== 'function') return;
+ if (!uci || typeof uci.unload !== 'function') return null;
/* `state.values` and `loaded` are private, so the same rule as L.Poll.timer applies: a shape we
* do not recognise is a reason to do nothing, once, loudly — never to guess. The two hold
* different halves of the cache (a package whose load is still in flight is in `loaded` alone),
@@ -141,14 +145,60 @@ function flushUciCache() {
+ 'will report a missing config on the second SPA visit. fs-router.js needs updating for '
+ 'this luci-base.');
}
- return;
+ return null;
}
const names = Object.keys(uci.state.values).concat(Object.keys(uci.loaded));
- if (names.length) uci.unload(names);
+ if (!names.length) return null;
+ uci.unload(names);
/* `state.reorder` is the one half unload() does not clear, and it is deliberately left alone
* rather than reset by hand: with `values` gone, reorderSections() finds no sections to order,
* emits no call and clears the map itself on the next save. Writing to that field would be us
* editing another module's private state, for a difference nothing can observe. */
+
+ /* …and put back the three packages luci-base's network.js reads but will never load again.
+ *
+ * initNetworkState() loads `network`, `wireless` and `luci` ONCE, fills its own `_state`, and
+ * from then on answers every caller with `return (_state != null ? Promise.resolve(_state) : _init)`
+ * — no uci call, ever again, for the life of the document. What it answers WITH, though, is the
+ * uci cache: `getWifiDevices()` is `uci.sections('wireless', 'wifi-device')`, and a view like
+ * network/switch reads `uci.sections('network', 'switch')` in its own render(). So dropping those
+ * packages does not make network.js refetch them — it makes every consumer read an EMPTY config
+ * until the next full load.
+ *
+ * Measured on 24.10, one SPA navigation away from Interfaces: `uci.state.values` {}, and
+ * `network.getWifiDevices()` 2 devices -> 0. That is Status -> Channel Analysis painting its title
+ * and its button with no band tabs under them, and Network -> Switch painting its description and
+ * Save/Apply with no VLAN sections — both correct again after F5, which is exactly how it was
+ * reported upstream ("only work after reloading the page"). tools/spa-parity.mjs reproduces it in
+ * one command, and tools/upstream-contract.mjs is what notices if that list of three ever moves.
+ *
+ * Refilling them is what a full load hands the incoming view, so navigate() WAITS for it (see the
+ * Promise.all below): a cached module resolves within a microtask, well before this request lands,
+ * and the view would read the empty cache we just left it. Only when network.js is really in the
+ * document — no module, no derived state to keep in step, and no request to spend. The ubus half
+ * of `_state` stays as stale as upstream leaves it; a view that needs it fresh calls
+ * network.flushCache() itself, and that is not ours to decide.
+ *
+ * WHAT IT COSTS, and why the cheaper shape was not taken. Awaiting this puts one uci `get` in
+ * front of the render on a navigation whose module is already cached — measured on the stand over
+ * 12 alternating navigations between two warm views: 136 ms median without the wait, 159 ms with
+ * it. `uci.load()` also dispatches `uci-loaded`, which luci-base wires to the change indicator, so
+ * a second, unawaited `uci.changes()` rides along that `unload()` alone never triggered.
+ *
+ * The free alternative is to leave these three OUT of the unload rather than drop and refetch
+ * them, and it was rejected on what it does to the pages people edit: `network.flushCache()` — the
+ * one call a view makes when it wants fresh data — reloads its ubus half but calls `uci.load()`
+ * for the uci half, which is a no-op while the package is still cached. Interfaces and Wireless
+ * would then render fresh device state over config values from whenever the document first
+ * touched them, which a full load never does. Dropping and refilling keeps both halves as fresh as
+ * a full load, and keeps pending local edits behaving as they do everywhere else: discarded by the
+ * navigation, exactly like the poll queue and the view's intervals above. */
+ if (!window.L.network) return null;
+ const refill = [ 'network', 'wireless', 'luci' ].filter((p) => names.indexOf(p) !== -1);
+ if (!refill.length) return null;
+ return uci.load(refill).catch((e) => {
+ console.error('footstrap: reloading uci ' + refill.join(', ') + ' after a navigation failed', e);
+ });
}
let _uciCacheWarned = false;
@@ -676,8 +726,9 @@ function navigate(pathname, push, kbd) {
* would have. L.Poll's own tick survives. */
clearViewIntervals();
/* and drop uci's document-scoped config cache, which a full load would not have carried into the
- * incoming page either (see flushUciCache) */
- flushUciCache();
+ * incoming page either (see flushUciCache). What it hands back is the refill of the packages
+ * network.js will never load again — awaited below, before the view renders. */
+ const uciWarm = flushUciCache();
/* the outgoing page's links are about to become a detached tree — do not hold one of them */
_lastHovered = null;
/* run every registered navigation callback — today the search palette's recent-pages record and
@@ -819,7 +870,10 @@ function navigate(pathname, push, kbd) {
* and the wait introduces a window in which we may never get there. Marking it up front would
* make the NEXT navigation take the cached branch — `new view.constructor()` on a class whose
* require() is what renders it — i.e. two renders and two pollers for one page. */
- warmedThen(className).then(() => {
+ /* `uciWarm` rides alongside the prefetch wait rather than after it: both are in flight already, and
+ * a view must not be constructed until the uci cache it reads is back (see flushUciCache). It never
+ * rejects — the refill catches its own failure — so it cannot cost the navigation a full reload. */
+ Promise.all([ warmedThen(className), uciWarm ]).then(() => {
/* superseded while waiting: never start the require. On a first visit the require IS the
* render, so starting it here would paint a page the user has already left and hand
* repairStaleRender() a mess that only exists because a require in flight cannot be stopped. */
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js
index eae87c8bc..967561153 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-select.js
@@ -591,7 +591,7 @@ const HOLDS_CONTROLS = '.cbi-dropdown, .cbi-dynlist, .cbi-tooltip-container, .cb
* `grid`/`treegrid`), so overwriting it with `group` drops every cell to generic — a screen reader
* would then read a flat run of text with no rows and no columns, which is the very structure the
* SC 1.4.10 exception exists to preserve. `aria-label` works on either, so the name and the tab stop
- * are unaffected. Reported in review.
+ * are unaffected.
*
* WHAT IS REMOVED IS WHAT WAS WRITTEN, remembered per element rather than inferred from the value:
* an app that had its own `tabindex="0"` or `role="group"` on that table got them taken away when the
@@ -639,18 +639,12 @@ function fitScrollables() {
if (scroll === was) { if (was) t.classList.add('fs-xscroll'); return; }
if (scroll) {
t.classList.add('fs-xscroll');
- /* A SCROLL BOX THE KEYBOARD CANNOT REACH IS CONTENT THE KEYBOARD CANNOT READ.
+ /* A SCROLL BOX THE KEYBOARD CANNOT REACH IS CONTENT THE KEYBOARD CANNOT READ, and the
+ * tab stop, the name and the conditional role that say so are all reach()'s business —
+ * its header above carries the reasoning.
*
- * Firefox has made scrollers focusable for years and Chrome since 132, but only when they
- * hold no focusable child — a table row full of buttons disqualifies itself — and WebKit
- * has not shipped it at all (bug 190870, open since 2018). So say it in the markup: a
- * tab stop (SC 2.1.1), a role and a name so what receives focus can be announced
- * (SC 4.1.2). `group`, not `region`: a region is a landmark, and a status page with four
- * scrolling tables would put four of them in the landmark list.
- *
- * The name comes from the heading the table sits under, which is what a sighted user
- * reads it as; only the fallback is ours to translate. Never overwritten: an app that
- * labelled its own table knows better. */
+ * `group`, not `region`: a region is a landmark, and a status page with four scrolling
+ * tables would put four of them in the landmark list. */
reach(t);
}
else if (was) unreach(t);
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-version.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-version.js
index 4aaf7da1b..8bb8aa1be 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-version.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-version.js
@@ -30,7 +30,7 @@ function isReal() { return ((/^\d+\.\d+/).test(FS_VERSION)) && !((/-dev$/).test(
return baseclass.extend({
VERSION: FS_VERSION,
/* the project's page, for the link at the foot of the Appearance tab. One string, used once —
- * it was assembled from a separate FS_REPO constant that the retired updater needed to build
+ * it was assembled from a separate FS_REPO constant that the retired self-update script needed to build
* API paths from, and nothing has needed the bare `owner/name` since. */
REPO_URL: 'https://github.com/VizzleTF/luci-theme-footstrap',
isReal,
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js b/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js
index 01d4427a4..dbe761579 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/menu-footstrap-common.js
@@ -58,6 +58,8 @@ return baseclass.extend({
fit.add(chrome.fitChrome);
chrome.renderChrome();
+ /* once per document — the footer outlives every client navigation */
+ chrome.nameThemeInFooter();
/* Both of these ADD to a stock page rather than owning a route: each watches
* body[data-page] and does nothing anywhere else. A theme may not register a
* dispatcher node — it outlives the theme that registered it. */
diff --git a/luci-theme-footstrap/styles/theme/20-shell.css b/luci-theme-footstrap/styles/theme/20-shell.css
index 5f7f3d891..193349a29 100644
--- a/luci-theme-footstrap/styles/theme/20-shell.css
+++ b/luci-theme-footstrap/styles/theme/20-shell.css
@@ -677,6 +677,17 @@
* this rule made them dead.) */
justify-content: center;
text-align: center;
+ /* AND SPACE THE LINES, because the two links in it become adjacent TARGETS the moment the
+ * line wraps. The footer is one sentence carrying the LuCI link and the OpenWrt link; on a
+ * wide screen they sit side by side and nothing is close to anything, but at the width where
+ * the sentence breaks they land on consecutive lines. With the inherited 1.5 that is a 16px
+ * line box, i.e. 16px between the two links' centres — inside the 24px envelope WCAG 2.2
+ * SC 2.5.8 requires around a target smaller than 24x24, which an inline link always is.
+ * Found by tools/live-audit.mjs on CI's runner and not on the maintainer's stands: the wrap
+ * width depends on text metrics, so the same 568px viewport breaks the line there and not
+ * here. 26px, not 24: the criterion is that the two 24px circles must not meet, so 24 is the
+ * boundary rather than a pass, and 26 leaves the footer visually unchanged on one line. */
+ line-height: 26px;
}
footer a { color: var(--fs-dim); }
footer a:hover { color: var(--fs-accent); }
diff --git a/natflow/Makefile b/natflow/Makefile
index fe7da8070..43b739fed 100644
--- a/natflow/Makefile
+++ b/natflow/Makefile
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260531
-PKG_RELEASE:=69
+PKG_RELEASE:=70
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git
-PKG_SOURCE_VERSION:=114c4a602c66cec71fe268455643e95c6df14695
+PKG_SOURCE_VERSION:=bbd26afc851575c8c1b326a5d0bdfa3e6c2723c1
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang
PKG_LICENSE:=GPL-2.0