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