mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 21:12:51 +08:00
🎄 Sync 2026-08-17 19:39:03
This commit is contained in:
1 parent
762c5f8cd4
commit
522b0fc0b6
58 files changed
+1231
-680
No files matched your search
@@ -8,10 +8,10 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=golang1.26
|
||||
GO_VERSION_MAJOR_MINOR:=1.26
|
||||
GO_VERSION_PATCH:=5
|
||||
GO_VERSION_PATCH:=6
|
||||
GO_VERSION_RC:=
|
||||
GO_BOOTSTRAP_VERSION:=bootstrap
|
||||
PKG_HASH:=495be4bc87176ac567392e5b4116abd98466d33d7b49d41e764ccc6976b2dc42
|
||||
PKG_HASH:=a0721c54c688901448d77ad9b3ec7ea7c474730755ff891382e92ecb93ff2cb1
|
||||
|
||||
PKG_VERSION:=$(GO_VERSION_MAJOR_MINOR)$(if $(GO_VERSION_RC),.0)$(if $(GO_VERSION_PATCH),.$(GO_VERSION_PATCH))
|
||||
PKG_FILE_VERSION:=$(GO_VERSION_MAJOR_MINOR)$(if $(GO_VERSION_RC),rc$(GO_VERSION_RC))$(if $(GO_VERSION_PATCH),.$(GO_VERSION_PATCH))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.2.1
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
LUCI_TITLE:=LuCI Support for momo
|
||||
LUCI_DEPENDS:=+luci-base +momo
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"/etc/momo/profiles/*.json": ["read"],
|
||||
"/etc/momo/subscriptions/*.json": ["read"],
|
||||
"/etc/momo/run/config.json": ["read"],
|
||||
"/var/log/momo/*.log": ["read"]
|
||||
"/var/log/momo/*.log": ["read"],
|
||||
"/tmp/log/momo/*.log": ["read"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
@@ -20,7 +21,8 @@
|
||||
"/etc/momo/profiles/*.json": ["write"],
|
||||
"/etc/momo/subscriptions/*.json": ["write"],
|
||||
"/etc/momo/run/config.json": ["write"],
|
||||
"/var/log/momo/*.log": ["write"]
|
||||
"/var/log/momo/*.log": ["write"],
|
||||
"/tmp/log/momo/*.log": ["write"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.26.1
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
|
||||
LUCI_TITLE:=LuCI Support for nikki
|
||||
LUCI_DEPENDS:=+luci-base +nikki
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"/etc/nikki/run/config.yaml": ["read"],
|
||||
"/etc/nikki/run/providers/rule/*": ["read"],
|
||||
"/etc/nikki/run/providers/proxy/*": ["read"],
|
||||
"/var/log/nikki/*.log": ["read"]
|
||||
"/var/log/nikki/*.log": ["read"],
|
||||
"/tmp/log/nikki/*.log": ["read"]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
@@ -30,7 +31,8 @@
|
||||
"/etc/nikki/run/config.yaml": ["write"],
|
||||
"/etc/nikki/run/providers/rule/*": ["write"],
|
||||
"/etc/nikki/run/providers/proxy/*": ["write"],
|
||||
"/var/log/nikki/*.log": ["write"]
|
||||
"/var/log/nikki/*.log": ["write"],
|
||||
"/tmp/log/nikki/*.log": ["write"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.8.12
|
||||
PKG_RELEASE:=224
|
||||
PKG_RELEASE:=225
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
@@ -17,7 +17,6 @@ PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Geoview \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Server \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \
|
||||
@@ -97,12 +96,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria
|
||||
select PACKAGE_hysteria
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy
|
||||
bool "Include NaiveProxy"
|
||||
depends on !(arc||armeb||loongarch64||mips||mips64||powerpc||TARGET_gemini)
|
||||
select PACKAGE_naiveproxy
|
||||
default n
|
||||
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Rust_Client
|
||||
bool "Include Shadowsocks Rust Client"
|
||||
depends on !i386
|
||||
|
||||
@@ -661,11 +661,16 @@ end
|
||||
function delete_select_nodes()
|
||||
local ids = http.formvalue("ids")
|
||||
local redirect = http.formvalue("redirect")
|
||||
local ids_t = {}
|
||||
string.gsub(ids, '[^' .. "," .. ']+', function(w)
|
||||
uci_foreach("socks", function(t)
|
||||
if t["node"] == w then
|
||||
uci_del(t[".name"])
|
||||
ids_t[#ids_t + 1] = w
|
||||
if (uci_get("@global[0]", "tcp_node") or "") == w then
|
||||
uci_del('@global[0]', "tcp_node")
|
||||
end
|
||||
if (uci_get("@global[0]", "udp_node") or "") == w then
|
||||
uci_del('@global[0]', "udp_node")
|
||||
end
|
||||
uci_foreach("socks", function(t)
|
||||
local changed = false
|
||||
local auto_switch_node_list = uci_get(t[".name"], "autoswitch_backup_node") or {}
|
||||
for i = #auto_switch_node_list, 1, -1 do
|
||||
@@ -677,13 +682,15 @@ function delete_select_nodes()
|
||||
if changed then
|
||||
uci_set(t[".name"], "autoswitch_backup_node", auto_switch_node_list)
|
||||
end
|
||||
if t["node"] == w then
|
||||
local new_node = api.get_random_normal_node(ids_t)
|
||||
if new_node then
|
||||
uci_set(t[".name"], "node", new_node[".name"])
|
||||
else
|
||||
uci_set(t[".name"], "enabled", "0")
|
||||
end
|
||||
end
|
||||
end)
|
||||
if (uci_get("@global[0]", "tcp_node") or "") == w then
|
||||
uci_del('@global[0]', "tcp_node")
|
||||
end
|
||||
if (uci_get("@global[0]", "udp_node") or "") == w then
|
||||
uci_del('@global[0]', "udp_node")
|
||||
end
|
||||
uci_foreach("haproxy_config", function(t)
|
||||
if t["lbss"] == w then
|
||||
uci_del(t[".name"])
|
||||
|
||||
@@ -716,6 +716,7 @@ o.rmempty = false
|
||||
|
||||
s2 = m:section(TypedSection, "socks", translate("Socks Config"))
|
||||
s2.template = "cbi/tblsection"
|
||||
s2.sortable = true
|
||||
s2.anonymous = true
|
||||
s2.addremove = true
|
||||
s2.extedit = api.url("socks_config", "%s")
|
||||
@@ -810,11 +811,13 @@ o.default = n + 1080
|
||||
o.datatype = "port"
|
||||
o.rmempty = false
|
||||
|
||||
--[[
|
||||
if has_singbox or has_xray then
|
||||
o = s2:option(Value, "http_port", "HTTP " .. translate("Listen Port"))
|
||||
o.default = 0
|
||||
o.datatype = "port"
|
||||
end
|
||||
]]--
|
||||
|
||||
local tcp = s.fields["tcp_node"]
|
||||
local udp = s.fields["udp_node"]
|
||||
@@ -863,5 +866,6 @@ end
|
||||
|
||||
m:appendTemplate("/global/footer", {shunt_list = api.jsonc.stringify(shunt_list)})
|
||||
m:appendTemplate("/global/status_bottom")
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s2.sectiontype})
|
||||
|
||||
return api.return_map(m)
|
||||
@@ -1,37 +0,0 @@
|
||||
local m, s = ...
|
||||
|
||||
if not api.is_finded("naive") then
|
||||
return
|
||||
end
|
||||
|
||||
type_name = "Naiveproxy"
|
||||
|
||||
-- [[ Naive ]]
|
||||
|
||||
s.fields["type"]:value(type_name, "NaiveProxy")
|
||||
|
||||
if s.val["type"] ~= type_name then
|
||||
return
|
||||
end
|
||||
|
||||
local option_prefix = "naive_"
|
||||
|
||||
local function _n(name)
|
||||
return option_prefix .. name
|
||||
end
|
||||
|
||||
o = s:option(ListValue, _n("protocol"), translate("Protocol"))
|
||||
o:value("https", translate("HTTPS"))
|
||||
o:value("quic", translate("QUIC"))
|
||||
|
||||
o = s:option(Value, _n("address"), translate("Address (Support Domain Name)"))
|
||||
|
||||
o = s:option(Value, _n("port"), translate("Port"))
|
||||
o.datatype = "port"
|
||||
|
||||
o = s:option(Value, _n("username"), translate("Username"))
|
||||
|
||||
o = s:option(Value, _n("password"), translate("Password"))
|
||||
o.password = true
|
||||
|
||||
api.luci_types(arg[1], m, s, type_name, option_prefix)
|
||||
@@ -565,7 +565,7 @@ function get_valid_nodes()
|
||||
local nodes = {}
|
||||
local default_nodes = {}
|
||||
local other_nodes = {}
|
||||
uci:foreach(c_config, "nodes", function(e)
|
||||
uci_foreach_c("nodes", function(e)
|
||||
e.id = e[".name"]
|
||||
if e.type and e.remarks then
|
||||
local type_name = e.type
|
||||
@@ -638,7 +638,7 @@ function get_node_list()
|
||||
socks_list = {},
|
||||
normal_list = {},
|
||||
}
|
||||
uci:foreach(c_config, "socks", function(s)
|
||||
uci_foreach_c("socks", function(s)
|
||||
if s.enabled == "1" and s.node then
|
||||
node_list.socks_list[#node_list.socks_list + 1] = {
|
||||
id = s[".name"],
|
||||
@@ -727,6 +727,29 @@ function get_full_node_remarks(n)
|
||||
return remarks
|
||||
end
|
||||
|
||||
function get_random_normal_node(exclude_id)
|
||||
local exclude_id_lookup = {}
|
||||
if type(exclude_id) == "table" then
|
||||
for i, v in ipairs(exclude_id) do
|
||||
exclude_id_lookup[v] = true
|
||||
end
|
||||
elseif type(exclude_id) == "string" then
|
||||
exclude_id_lookup[exclude_id] = true
|
||||
end
|
||||
local normal_list = {}
|
||||
for k, e in ipairs(get_valid_nodes()) do
|
||||
if e.node_type == "normal" and not exclude_id_lookup[e[".name"]] then
|
||||
normal_list[#normal_list + 1] = e
|
||||
end
|
||||
end
|
||||
if #normal_list > 0 then
|
||||
math.randomseed(os.time())
|
||||
local num = math.random(1, #normal_list)
|
||||
return normal_list[num]
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function gen_uuid()
|
||||
local uuid = sys.exec("echo -n $(cat /proc/sys/kernel/random/uuid)")
|
||||
return uuid
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
module("luci.passwall.util_naiveproxy", package.seeall)
|
||||
local api = require "luci.passwall.api"
|
||||
local jsonc = api.jsonc
|
||||
|
||||
function gen_config(var)
|
||||
local node_id = var["node"]
|
||||
if not node_id then
|
||||
print("node 不能为空")
|
||||
return
|
||||
end
|
||||
local node = api.uci_get_c(node_id)
|
||||
local run_type = var["run_type"]
|
||||
local local_addr = var["local_addr"]
|
||||
local local_port = var["local_port"]
|
||||
local server_host = var["server_host"] or (node.address or ""):lower()
|
||||
local server_port = var["server_port"] or node.port
|
||||
|
||||
if api.is_ipv6(server_host) then
|
||||
server_host = api.get_ipv6_full(server_host)
|
||||
end
|
||||
local server = server_host .. ":" .. server_port
|
||||
|
||||
local config = {
|
||||
listen = run_type .. "://" .. local_addr .. ":" .. local_port,
|
||||
proxy = node.protocol .. "://" .. node.username .. ":" .. node.password .. "@" .. server
|
||||
}
|
||||
|
||||
return jsonc.stringify(config, 1)
|
||||
end
|
||||
|
||||
_G.gen_config = gen_config
|
||||
|
||||
if arg[1] then
|
||||
local func =_G[arg[1]]
|
||||
if func then
|
||||
local var = nil
|
||||
if arg[2] then
|
||||
var = jsonc.parse(arg[2])
|
||||
end
|
||||
print(func(var))
|
||||
end
|
||||
end
|
||||
@@ -45,6 +45,8 @@ chmod +x /usr/share/passwall/*.sh
|
||||
uci -q delete passwall.@global_xray[0].sniffing
|
||||
uci -q delete passwall.@global_xray[0].route_only
|
||||
|
||||
[ "$(uci -q get passwall.@global_haproxy[0].health_check_type)" = "passwall_logic" ] && uci -q set passwall.@global_haproxy[0].health_check_type='script_logic'
|
||||
|
||||
for sid in $(uci show passwall | grep "=socks" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}'); do
|
||||
if [ -z "$(echo ${sid} | grep '^socks_')" ]; then
|
||||
new_id="socks_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
|
||||
|
||||
@@ -13,7 +13,6 @@ LUA_UTIL_PATH=/usr/lib/lua/luci/passwall
|
||||
UTIL_SINGBOX=$LUA_UTIL_PATH/util_sing-box.lua
|
||||
UTIL_SS=$LUA_UTIL_PATH/util_shadowsocks.lua
|
||||
UTIL_XRAY=$LUA_UTIL_PATH/util_xray.lua
|
||||
UTIL_NAIVE=$LUA_UTIL_PATH/util_naiveproxy.lua
|
||||
UTIL_HYSTERIA2=$LUA_UTIL_PATH/util_hysteria2.lua
|
||||
SINGBOX_BIN=$(first_type $(config_t_get global_app sing_box_file) sing-box)
|
||||
XRAY_BIN=$(first_type $(config_t_get global_app xray_file) xray)
|
||||
@@ -456,13 +455,6 @@ run_socks() {
|
||||
[ -n "$no_run" ] && _args="${_args} no_run=1"
|
||||
run_xray flag=$flag node=$node socks_address=$bind socks_port=$socks_port config_file=$config_file log_file=$log_file ${_args}
|
||||
;;
|
||||
naiveproxy)
|
||||
json_add_string "run_type" "socks"
|
||||
json_add_string "local_addr" "$bind"
|
||||
json_add_string "local_port" "$socks_port"
|
||||
lua $UTIL_NAIVE gen_config "$(json_dump)" > $config_file
|
||||
[ -n "$no_run" ] || ln_run "$(first_type naive)" naive $log_file "$config_file"
|
||||
;;
|
||||
ssr)
|
||||
json_add_string "local_addr" "$bind"
|
||||
json_add_string "local_port" "$socks_port"
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.8.16
|
||||
PKG_RELEASE:=89
|
||||
PKG_VERSION:=26.8.17
|
||||
PKG_RELEASE:=90
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
'require form';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callInitAction = rpc.declare({
|
||||
object: 'luci',
|
||||
method: 'setInitAction',
|
||||
params: ['name', 'action'],
|
||||
expect: { result: false }
|
||||
});
|
||||
|
||||
const callUpnpGetStatus = rpc.declare({
|
||||
object: 'luci.upnp',
|
||||
method: 'get_status',
|
||||
@@ -350,7 +343,7 @@ return view.extend({
|
||||
o.modalonly = false;
|
||||
|
||||
o = s.option(form.Value, 'int_addr', _('IP address'),
|
||||
_('Enter device\'s IPv4 address, or an address with netmask'));
|
||||
_('Enter device\'s IPv4 address, or an address and netmask'));
|
||||
o.datatype = 'ip4addr';
|
||||
o.rmempty = false;
|
||||
o.retain = true;
|
||||
|
||||
@@ -3,8 +3,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2009-05-19 19:36+0200\n"
|
||||
"PO-Revision-Date: 2025-11-28 22:04+0000\n"
|
||||
"Last-Translator: niky1987 <niky1987@libero.it>\n"
|
||||
"PO-Revision-Date: 2026-07-23 06:18+0000\n"
|
||||
"Last-Translator: Random <random-r@users.noreply.hosted.weblate.org>\n"
|
||||
"Language-Team: Italian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsupnp/it/>\n"
|
||||
"Language: it\n"
|
||||
@@ -12,7 +12,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.15-dev\n"
|
||||
"X-Generator: Weblate 2026.8.dev0\n"
|
||||
|
||||
#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:183
|
||||
msgctxt ""
|
||||
@@ -119,7 +119,7 @@ msgstr "Velocità di download"
|
||||
|
||||
#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:144
|
||||
msgid "Enable PCP/NAT-PMP protocols"
|
||||
msgstr "Abilita il protocollo PCP/NAT-PMP"
|
||||
msgstr "Attiva i protocolli PCP/NAT-PMP"
|
||||
|
||||
#: applications/luci-app-upnp/htdocs/luci-static/resources/view/upnp/upnp.js:141
|
||||
msgid "Enable UPnP IGD protocol"
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"description": "Grant access to UPnP IGD & PCP/NAT-PMP service configuration",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.upnp": [ "get_status" ],
|
||||
"luci": [ "setInitAction" ]
|
||||
"luci.upnp": [ "get_status" ]
|
||||
},
|
||||
"uci": [ "upnpd" ]
|
||||
},
|
||||
|
||||
@@ -11,10 +11,10 @@ address, so they stay local).
|
||||
|
||||
| Metric | Budget | Track | Source |
|
||||
|---|---|---|---|
|
||||
| main.css (identity/raw) | ≤ 193 KB | size | production build, 2026-08 (192,201 B; +~2 KB custom-background mode, +~0.5 KB router progress bar / live region) |
|
||||
| main.css (identity/raw) | ≤ 193 KB | size | production build, 2026-08 (191,899 B; +~2 KB custom-background mode, +~0.5 KB router progress bar / live region; the bar is now Turbo-shaped — width/opacity only, inserted per navigation) |
|
||||
| login.css (identity/raw) | ≤ 12 KB | size | production build, 2026-07 (10,935 B, token-pruned) |
|
||||
| menu-aurora.js (identity/raw) | ≤ 21.5 KB | size | production build, 2026-08 (20,851 B; +1.3 KB for the router's `syncRoute`/`closeSurfaces` hooks) |
|
||||
| router-aurora.js (identity/raw, Navigation-API browsers only) | ≤ 15 KB | size | production build, 2026-08 (14,568 B: template shells fetched, per-render listener teardown, timeout-as-failure, expiry gate, readonly folding, node css, wildcard actions, progress bar, visibility gate, contract check) |
|
||||
| router-aurora.js (identity/raw, Navigation-API browsers only) | ≤ 15 KB | size | production build, 2026-08 (14,844 B: template shells fetched, per-render listener teardown, timeout-as-failure, expiry gate, readonly folding, node css, wildcard actions, trickling progress bar, same-URL reload rule, visibility gate, contract check) |
|
||||
| Default logo (identity/raw) | ≤ 16 KB | size | production build, 2026-07 (15,057 B) |
|
||||
| Core admin cold theme assets (identity/raw) | ≤ 267 KB | size | main CSS + menu JS + router JS + default font + logo, 2026-08 (≈266.3 KB; the router is a one-time cost that removes per-click dispatcher work) |
|
||||
| Login cold theme assets, excluding configured background (identity/raw) | ≤ 55 KB | size | login CSS + default font + logo, 2026-07 (49,572 B) |
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
* on the walk): reached same-document, the navigation away is a
|
||||
* full load (poison gate), the one after is same-document again.
|
||||
* Skipped when the walk found no such page.
|
||||
* hygiene progress bar and live region exist; a hidden tab stops polling
|
||||
* and a visible one resumes it; the live region carries the title.
|
||||
* hygiene no progress bar remains after a swap (it is inserted for the
|
||||
* navigation and removed after its fade), the live region carries
|
||||
* the title, a hidden tab stops polling and a visible one resumes it.
|
||||
* nodecss a page whose menu.d node declares `css`: its link is enabled on
|
||||
* arrival, disabled (not removed) after leaving, re-enabled without
|
||||
* a duplicate on return. Skipped when no such node is installed.
|
||||
@@ -495,7 +496,7 @@ if (!ONLY || ONLY === "hygiene") {
|
||||
const home = await spaNavigate(p.sessionId, START);
|
||||
await waitViewSettled(p.sessionId); await sleep(1500);
|
||||
const r = JSON.parse(await evaljs(p.sessionId, `(async () => {
|
||||
const bar = !!document.getElementById('aurora-nav-progress');
|
||||
const barGone = !document.getElementById('aurora-nav-progress');
|
||||
const status = document.getElementById('aurora-nav-status')?.textContent;
|
||||
const wasActive = L.Poll.active();
|
||||
Object.defineProperty(document, 'hidden', { configurable: true, get: () => true });
|
||||
@@ -504,10 +505,10 @@ if (!ONLY || ONLY === "hygiene") {
|
||||
Object.defineProperty(document, 'hidden', { configurable: true, get: () => false });
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
const shownActive = L.Poll.active();
|
||||
return JSON.stringify({ bar, status, title: document.title, wasActive, hiddenActive, shownActive });
|
||||
return JSON.stringify({ barGone, status, title: document.title, wasActive, hiddenActive, shownActive });
|
||||
})()`, true));
|
||||
out.hygiene = { ...r, sameDoc: nav.sameDoc && home.sameDoc,
|
||||
ok: nav.sameDoc && home.sameDoc && r.bar && r.status === r.title && r.wasActive && !r.hiddenActive && r.shownActive };
|
||||
ok: nav.sameDoc && home.sameDoc && r.barGone && r.status === r.title && r.wasActive && !r.hiddenActive && r.shownActive };
|
||||
}
|
||||
|
||||
/* ---------- menu.d node css ---------- */
|
||||
|
||||
@@ -160,6 +160,16 @@ A `navigate` event is intercepted only when **all** hold:
|
||||
|
||||
- `event.canIntercept` (same-origin, not cross-document-only), not
|
||||
`hashChange`, no `downloadRequest`, no `formData`, `navigationType !== 'reload'`;
|
||||
- the destination is **not the document's own URL** (fragment aside). A
|
||||
same-URL navigation arrives as `navigationType: 'replace'`, not
|
||||
`'reload'`, yet it is a reload by another name: luci-base's
|
||||
`ui.changes.apply/revert` end in `window.location =
|
||||
window.location.href.split('#')[0]` (and the expiry modal's button in the
|
||||
same) precisely so the server re-renders the shell — a theme switched
|
||||
under System → Language and Style, a new language, a new hostname, a
|
||||
changed menu tree — and intercepting it left the swap showing the old
|
||||
shell until F5. A click on the current page's own link is the same reload
|
||||
it is in the MPA;
|
||||
- the destination path (minus `L.env.scriptname`) resolves in the menu tree
|
||||
to a **serviceable node** (below);
|
||||
- the document is not **poisoned** (below) and its session is not known to
|
||||
@@ -392,12 +402,22 @@ handler in order:
|
||||
8. **Focus and announcement.** `#maincontent` (`tabindex=-1`) with
|
||||
`preventScroll`; the new `document.title` is written into
|
||||
`#aurora-nav-status` (`role=status`, `aria-live=polite`), since a
|
||||
same-document swap fires no load a screen reader would announce.
|
||||
9. **Progress.** A navigation that outlives 150 ms shows `#aurora-nav-progress`,
|
||||
a hairline at the top that creeps right while the view renders and
|
||||
completes on commit (`data-state` active → done); shorter ones stay
|
||||
silent, overlapping ones share the bar. Reduced motion drops its
|
||||
transitions, not the bar.
|
||||
same-document swap fires no load a screen reader would announce. The
|
||||
landmark carries `outline-none`: iOS WebKit (Safari and Chrome for iOS
|
||||
alike) paints its focus ring for programmatic focus, and the ring's top
|
||||
edge just under the sticky header was reported as "a progress bar that
|
||||
never goes away" — it was never the bar.
|
||||
9. **Progress.** A navigation that outlives 150 ms gets `#aurora-nav-progress`
|
||||
inserted — Turbo Drive's bar, in shape: a hairline at the top whose
|
||||
`width` is driven inline and **trickles** in ever-smaller steps
|
||||
(`+ (100 - w) / 30` every 300 ms) until commit, so a slow render keeps
|
||||
visibly moving instead of looking stuck; on commit it fills to 100 %,
|
||||
fades (`data-state="done"`) and is **removed from the DOM**. Shorter
|
||||
navigations stay silent, overlapping ones share the bar. The browser's
|
||||
own progress bar is no help here: it only shows for document loads,
|
||||
which is exactly what a same-document swap is not — hence GitHub,
|
||||
YouTube, Turbo/HEY and every nprogress user draw their own. Reduced
|
||||
motion drops the transitions, not the bar.
|
||||
10. Any exception → `console.error` (a silent fallback makes every router
|
||||
regression look like "the page is just slow") → `location.href =
|
||||
destination` — a hard full load, never a stuck page.
|
||||
@@ -491,8 +511,8 @@ already 0-byte cache hits.
|
||||
- Unit (`.dev/tests/router.test.js`): resolver against a fixture tree
|
||||
(alias chain, nested firstchild, weights, ineligible, unsatisfied,
|
||||
wildcard args, cycle); URL → segments; patch prefix matching; pragma scan
|
||||
on a minified head; readonly folding; expiry signals; node css of the
|
||||
resolved leaf; the contract check.
|
||||
on a minified head; readonly folding; expiry signals; the same-URL reload
|
||||
rule; node css of the resolved leaf; the contract check.
|
||||
- Device (`.claude/skills/aurora-performance/scripts/bench-router.mjs`, CDP):
|
||||
1. full walk of every clickable node in each nav mode, each compared
|
||||
against a real full load of the same URL — `data-page`,
|
||||
@@ -507,8 +527,9 @@ already 0-byte cache hits.
|
||||
own sheets (found on the walk) instead of an injected one — reached
|
||||
same-document and landed on directly (its modules insert before the
|
||||
router boots), leaving is a full load either way;
|
||||
5c. hygiene: progress bar and live region present, live region carries the
|
||||
title, a hidden tab stops polling and a visible one resumes it;
|
||||
5c. hygiene: no progress bar left in the DOM after a swap, live region
|
||||
present and carrying the title, a hidden tab stops polling and a
|
||||
visible one resumes it;
|
||||
6. nodecss: a page whose menu.d node declares `css` — link enabled on
|
||||
arrival, disabled after leaving, re-enabled without a duplicate on
|
||||
return (skipped when no installed node declares one);
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* fixed pseudo made the same var(--bg) look mismatched).
|
||||
*/
|
||||
html {
|
||||
@apply bg-bg min-h-dvh font-sans;
|
||||
@apply bg-bg min-h-dvh font-sans scheme-light;
|
||||
|
||||
&[data-darkmode="true"] {
|
||||
@apply scheme-dark;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-bg min-h-dvh;
|
||||
|
||||
@@ -551,18 +551,16 @@ body[data-nav-type="sidebar"] {
|
||||
}
|
||||
}
|
||||
|
||||
/* Router progress (router.md): a hairline across the top that only
|
||||
appears once a navigation outlives its silent 150 ms, creeps towards
|
||||
the right while the view renders, and completes on commit. */
|
||||
/* Router progress (router.md) — Turbo Drive's bar: inserted by the router
|
||||
once a navigation outlives its silent 150 ms, width driven inline and
|
||||
trickling until commit (so a slow render keeps visibly moving), then
|
||||
filled, faded and removed from the DOM. Only width and opacity move.
|
||||
Reduced motion drops the transitions, not the bar. */
|
||||
#aurora-nav-progress {
|
||||
@apply from-progress-start to-progress-end pointer-events-none fixed inset-x-0 top-0 z-80 h-0.5 origin-left scale-x-0 bg-linear-to-r opacity-0;
|
||||
|
||||
&[data-state="active"] {
|
||||
@apply scale-x-90 opacity-100 transition-[scale,opacity] duration-[8s] ease-out;
|
||||
}
|
||||
@apply from-progress-start to-progress-end pointer-events-none fixed top-0 left-0 z-80 h-0.5 w-0 bg-linear-to-r transition-[width] duration-300 ease-out;
|
||||
|
||||
&[data-state="done"] {
|
||||
@apply scale-x-100 opacity-0 transition-[scale,opacity] duration-300 ease-out;
|
||||
@apply opacity-0 [transition:width_.3s_ease-out,opacity_.15s_ease-in_.15s];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +574,11 @@ body[data-nav-type="sidebar"] {
|
||||
/* Centred shells (top-bar navigation) clamp here and gutter the remainder.
|
||||
The sidebar shell overrides this — it caps the shell instead, see
|
||||
body[data-nav-type="sidebar"] above. */
|
||||
@apply mx-auto min-h-[calc(100vh-4rem)] w-23/24 max-w-(--content-width-centered) px-4 max-md:w-full max-md:px-3;
|
||||
@apply mx-auto min-h-[calc(100vh-4rem)] w-23/24 max-w-(--content-width-centered) px-4 outline-none max-md:w-full max-md:px-3;
|
||||
/* outline-none: the router focuses this landmark (tabindex=-1) after a
|
||||
same-document swap for assistive tech. iOS WebKit paints its focus ring
|
||||
for programmatic focus, and the ring's top edge just under the sticky
|
||||
header read as a stuck progress bar; a landmark needs no visible ring. */
|
||||
|
||||
/* Router staging (router.md): the incoming region keeps its full
|
||||
width for layout-measuring renders but takes no space and no paint. */
|
||||
|
||||
@@ -196,9 +196,6 @@ return baseclass.extend({
|
||||
const current = this.initialRoute();
|
||||
if (!current) return;
|
||||
document.querySelector('script[type="speculationrules"]')?.remove();
|
||||
this.bar = document.body.appendChild(
|
||||
E("div", { id: "aurora-nav-progress", "aria-hidden": "true" }),
|
||||
);
|
||||
this.status = document.body.appendChild(
|
||||
E("div", {
|
||||
id: "aurora-nav-status",
|
||||
@@ -263,23 +260,43 @@ return baseclass.extend({
|
||||
});
|
||||
},
|
||||
|
||||
// Turbo Drive's progress bar, in shape: the element exists only while a
|
||||
// navigation shows it (inserted after PROGRESS_DELAY, removed after the
|
||||
// done fade), its width is driven inline and trickles in ever-smaller
|
||||
// steps until commit, so a slow render never looks stuck. Overlapping
|
||||
// navigations share the bar.
|
||||
progressStart() {
|
||||
this.pending = (this.pending ?? 0) + 1;
|
||||
clearTimeout(this.progressTimer);
|
||||
this.progressTimer = setTimeout(
|
||||
() => (this.bar.dataset.state = "active"),
|
||||
PROGRESS_DELAY,
|
||||
this.progressTimer = setTimeout(() => {
|
||||
if (this.bar) return;
|
||||
this.bar = document.body.appendChild(
|
||||
E("div", { id: "aurora-nav-progress", "aria-hidden": "true" }),
|
||||
);
|
||||
this.bar.offsetWidth; // commit the resting width so the first step transitions
|
||||
this.trickle(10);
|
||||
}, PROGRESS_DELAY);
|
||||
},
|
||||
|
||||
trickle(value) {
|
||||
if (!this.bar) return;
|
||||
this.bar.style.width = `${value}%`;
|
||||
this.trickleTimer = setTimeout(
|
||||
() => this.trickle(value + (100 - value) / 30),
|
||||
300,
|
||||
);
|
||||
},
|
||||
|
||||
progressEnd() {
|
||||
if (--this.pending > 0) return;
|
||||
clearTimeout(this.progressTimer);
|
||||
if (this.bar.dataset.state !== "active") return;
|
||||
this.bar.dataset.state = "done";
|
||||
this.progressTimer = setTimeout(() => {
|
||||
if (this.bar.dataset.state === "done") delete this.bar.dataset.state;
|
||||
}, 300);
|
||||
clearTimeout(this.trickleTimer);
|
||||
const bar = this.bar;
|
||||
if (!bar) return;
|
||||
this.bar = null;
|
||||
bar.dataset.state = "done";
|
||||
bar.style.width = "100%";
|
||||
setTimeout(() => bar.remove(), 300);
|
||||
},
|
||||
|
||||
sheets() {
|
||||
@@ -371,6 +388,15 @@ return baseclass.extend({
|
||||
return data?.access === false;
|
||||
},
|
||||
|
||||
// A navigation to the document's own URL is a reload by another name:
|
||||
// luci-base reloads after apply/revert with `window.location = href` (a
|
||||
// "replace" navigation, not a "reload"), expecting the server to re-render
|
||||
// theme, language, hostname and menu — as does a click on the current
|
||||
// page's own link. Those go to the server like an F5.
|
||||
reloads(url) {
|
||||
return url.split("#")[0] === window.location.href.split("#")[0];
|
||||
},
|
||||
|
||||
nodespec(r) {
|
||||
return { ...r.node, readonly: readonlyAlong(this.tree, r.path) };
|
||||
},
|
||||
@@ -635,7 +661,8 @@ return baseclass.extend({
|
||||
ev.hashChange ||
|
||||
ev.downloadRequest !== null ||
|
||||
ev.formData ||
|
||||
ev.navigationType === "reload"
|
||||
ev.navigationType === "reload" ||
|
||||
this.reloads(ev.destination.url)
|
||||
)
|
||||
return;
|
||||
|
||||
@@ -692,6 +719,8 @@ return baseclass.extend({
|
||||
await this.commit(view);
|
||||
this.mountPatches(patches);
|
||||
this.status.textContent = document.title;
|
||||
// #maincontent is outline-none (_layout.css): iOS WebKit rings
|
||||
// programmatic focus, and that ring read as a stuck progress bar.
|
||||
document.getElementById("maincontent")?.focus({ preventScroll: true });
|
||||
} catch (err) {
|
||||
console.error("router-aurora:", err);
|
||||
|
||||
@@ -336,6 +336,18 @@ test("session expiry is recognised from the same signals luci-base uses", () =>
|
||||
assert.equal(router.sessionGone(null, probe), false);
|
||||
});
|
||||
|
||||
test("a navigation to the document's own URL is a reload, not a swap", () => {
|
||||
const router = loadRouter({ tree });
|
||||
const here = "https://r/cgi-bin/luci/admin/status/overview";
|
||||
|
||||
// luci-base's post-apply/revert `window.location = href.split('#')[0]`
|
||||
// and the expiry modal's `protocol//host pathname search` are both this.
|
||||
assert.equal(router.reloads(here), true);
|
||||
assert.equal(router.reloads(`${here}#tab`), true);
|
||||
assert.equal(router.reloads(`${here}?x=1`), false);
|
||||
assert.equal(router.reloads("https://r/cgi-bin/luci/admin/status/logs"), false);
|
||||
});
|
||||
|
||||
test("a menu.d node css is served for the resolved leaf only", () => {
|
||||
const t = {
|
||||
action: { type: "firstchild" },
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=1.2.1
|
||||
PKG_RELEASE:=67
|
||||
PKG_VERSION:=1.2.3
|
||||
PKG_RELEASE:=68
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content:"";--tw-shadow:0 0 #0000;--tw-inset-shadow:0 0 #0000;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base) * 2);--animate-spin:spin 1s linear infinite}}@layer base{*,:before,:after{box-sizing:border-box}body,hr,p,button,input{margin:0}button,input{font:inherit}a{text-decoration-line:none}[hidden]{display:none!important}}@layer components,utilities;:root{--bg:#f7fafc;--surface:#fff;--text:#121a22;--brand:#0085b7;--on-brand:#fff;--danger:#8d1925;--text-muted:#5f666d;--text-subtle:#7f858b;--hairline:#121a2221;--brand-subtle:#ddecf5;--danger-surface:#ffe8e6;--control-bg:#fff;--app-shadow-md:0 4px 16px #00000014, 0 1px 3px #0000000a;--font-sans:"Lato", ui-sans-serif, system-ui, sans-serif;--spacing:.25rem;--radius-base:.5rem}@supports (color:lab(0% 0 0)){:root{--bg:lab(98.1553% -.760227 -1.25223);--surface:lab(100% 0 0);--text:lab(8.89727% -1.68239 -6.93074);--brand:lab(51.3503% -19.237 -40.2116);--on-brand:lab(100% 0 0);--danger:lab(31.1495% 47.9256 25.8661);--text-muted:lab(42.8254% -1.57365 -4.8763);--text-subtle:lab(55.3248% -1.4126 -4.053);--hairline:lab(8.89727% -1.68239 -6.93074/.13);--brand-subtle:lab(92.5794% -3.9458 -6.17858);--danger-surface:lab(93.849% 11.2841 5.29771);--control-bg:lab(100.011% -.760317 -1.25225);--app-shadow-md:0 4px 16px lab(0% 0 0/.08), 0 1px 3px lab(0% 0 0/.04)}}[data-darkmode=true]{--bg:#05070e;--surface:#141822;--text:#f9fafb;--brand:#00958e;--on-brand:#fff;--danger:#f17070;--text-muted:#909297;--text-subtle:#5d6067;--hairline:#f9fafb1a;--brand-subtle:#061a20;--danger-surface:#541f20;--control-bg:#0a0e17;--app-shadow-md:0 10px 28px #0000006b}@supports (color:lab(0% 0 0)){[data-darkmode=true]{--bg:lab(1.93894% .0855997 -3.22397);--surface:lab(8.24713% -.0405461 -7.31381);--text:lab(98.252% -.0618994 -.756907);--brand:lab(55.0978% -44.5567 -7.62978);--on-brand:lab(100% 0 0);--danger:lab(63.5705% 50.8105 25.3639);--text-muted:lab(60.5379% -.225931 -3.05105);--text-subtle:lab(40.6865% -.278354 -4.23647);--hairline:lab(98.252% -.0618994 -.756907/.1);--brand-subtle:lab(7.95734% -6.34954 -6.72844);--danger-surface:lab(20.3014% 25.4342 12.827);--control-bg:lab(3.9802% .0812709 -5.64623);--app-shadow-md:0 10px 28px lab(0% 0 0/.42)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-property:none!important;animation:none!important}.spinning:before{animation:var(--animate-spin)!important}}html{background-color:var(--bg);min-height:100dvh;font-family:var(--font-sans)}html body{background-color:var(--bg);min-height:100dvh}.page-bg{pointer-events:none;background-image:var(--page-bg-lqip,var(--page-bg));background-position:50%;background-repeat:no-repeat;background-size:cover;position:fixed;inset:0;transform:translateZ(0)}.page-bg:after{pointer-events:none;background-image:var(--page-bg);opacity:0;--tw-content:"";content:var(--tw-content);background-position:50%;background-repeat:no-repeat;background-size:cover;transition:opacity .5s;position:absolute;inset:0}.page-bg.full-loaded:after{opacity:1}@keyframes divider-in{0%{opacity:0;transform:scaleX(.15)}}.login-screen{min-height:100dvh;padding-inline:calc(var(--spacing) * 5);padding-bottom:calc(var(--spacing) * 14);flex-direction:column;justify-content:center;align-items:center;display:flex;position:relative}.login-shell{width:100%;max-width:var(--container-md)}.page-bg{--page-bg:var(--login-bg);--page-bg-lqip:var(--login-bg-lqip)}body[data-bg] .page-bg:before{pointer-events:none;z-index:1;--tw-content:"";content:var(--tw-content);background:var(--bg);position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){body[data-bg] .page-bg:before{background:color-mix(in srgb,var(--bg) var(--login-bg-scrim,0%),transparent)}}body[data-bg] .login-card{backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);background:var(--surface)}@supports (color:color-mix(in lab, red, red)){body[data-bg] .login-card{background:color-mix(in srgb,var(--surface) var(--login-bg-alpha,100%),transparent)}}@Line truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-content:"";--tw-shadow:0 0 #0000;--tw-inset-shadow:0 0 #0000;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-shadow:0 0 #0000;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--spacing:var(--spacing);--container-md:28rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-widest:.1em;--radius-2xl:calc(var(--radius-base) * 2);--animate-spin:spin 1s linear infinite}}@layer base{*,:before,:after{box-sizing:border-box}body,hr,p,button,input{margin:0}button,input{font:inherit}a{text-decoration-line:none}[hidden]{display:none!important}}@layer components,utilities;:root{--bg:#f7fafc;--surface:#fff;--text:#121a22;--brand:#0085b7;--on-brand:#fff;--danger:#8d1925;--text-muted:#5f666d;--text-subtle:#7f858b;--hairline:#121a2221;--brand-subtle:#ddecf5;--danger-surface:#ffe8e6;--control-bg:#fff;--app-shadow-md:0 4px 16px #00000014, 0 1px 3px #0000000a;--font-sans:"Lato", ui-sans-serif, system-ui, sans-serif;--spacing:.25rem;--radius-base:.5rem}@supports (color:lab(0% 0 0)){:root{--bg:lab(98.1553% -.760227 -1.25223);--surface:lab(100% 0 0);--text:lab(8.89727% -1.68239 -6.93074);--brand:lab(51.3503% -19.237 -40.2116);--on-brand:lab(100% 0 0);--danger:lab(31.1495% 47.9256 25.8661);--text-muted:lab(42.8254% -1.57365 -4.8763);--text-subtle:lab(55.3248% -1.4126 -4.053);--hairline:lab(8.89727% -1.68239 -6.93074/.13);--brand-subtle:lab(92.5794% -3.9458 -6.17858);--danger-surface:lab(93.849% 11.2841 5.29771);--control-bg:lab(100.011% -.760317 -1.25225);--app-shadow-md:0 4px 16px lab(0% 0 0/.08), 0 1px 3px lab(0% 0 0/.04)}}[data-darkmode=true]{--bg:#05070e;--surface:#141822;--text:#f9fafb;--brand:#00958e;--on-brand:#fff;--danger:#f17070;--text-muted:#909297;--text-subtle:#5d6067;--hairline:#f9fafb1a;--brand-subtle:#061a20;--danger-surface:#541f20;--control-bg:#0a0e17;--app-shadow-md:0 10px 28px #0000006b}@supports (color:lab(0% 0 0)){[data-darkmode=true]{--bg:lab(1.93894% .0855997 -3.22397);--surface:lab(8.24713% -.0405461 -7.31381);--text:lab(98.252% -.0618994 -.756907);--brand:lab(55.0978% -44.5567 -7.62978);--on-brand:lab(100% 0 0);--danger:lab(63.5705% 50.8105 25.3639);--text-muted:lab(60.5379% -.225931 -3.05105);--text-subtle:lab(40.6865% -.278354 -4.23647);--hairline:lab(98.252% -.0618994 -.756907/.1);--brand-subtle:lab(7.95734% -6.34954 -6.72844);--danger-surface:lab(20.3014% 25.4342 12.827);--control-bg:lab(3.9802% .0812709 -5.64623);--app-shadow-md:0 10px 28px lab(0% 0 0/.42)}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-property:none!important;animation:none!important}.spinning:before{animation:var(--animate-spin)!important}}html{background-color:var(--bg);min-height:100dvh;font-family:var(--font-sans);color-scheme:light}html[data-darkmode=true]{color-scheme:dark}html body{background-color:var(--bg);min-height:100dvh}.page-bg{pointer-events:none;background-image:var(--page-bg-lqip,var(--page-bg));background-position:50%;background-repeat:no-repeat;background-size:cover;position:fixed;inset:0;transform:translateZ(0)}.page-bg:after{pointer-events:none;background-image:var(--page-bg);opacity:0;--tw-content:"";content:var(--tw-content);background-position:50%;background-repeat:no-repeat;background-size:cover;transition:opacity .5s;position:absolute;inset:0}.page-bg.full-loaded:after{opacity:1}@keyframes divider-in{0%{opacity:0;transform:scaleX(.15)}}.login-screen{min-height:100dvh;padding-inline:calc(var(--spacing) * 5);padding-bottom:calc(var(--spacing) * 14);flex-direction:column;justify-content:center;align-items:center;display:flex;position:relative}.login-shell{width:100%;max-width:var(--container-md)}.page-bg{--page-bg:var(--login-bg);--page-bg-lqip:var(--login-bg-lqip)}body[data-bg] .page-bg:before{pointer-events:none;z-index:1;--tw-content:"";content:var(--tw-content);background:var(--bg);position:absolute;inset:0}@supports (color:color-mix(in lab, red, red)){body[data-bg] .page-bg:before{background:color-mix(in srgb,var(--bg) var(--login-bg-scrim,0%),transparent)}}body[data-bg] .login-card{backdrop-filter:blur(var(--login-bg-blur,0px)) saturate(150%);background:var(--surface)}@supports (color:color-mix(in lab, red, red)){body[data-bg] .login-card{background:color-mix(in Line truncated
|
||||
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-content:"";--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1Line truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-content:"";--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-border-spacing-x:0;--tw-border-spacing-y:0}}}@layer theme{:root,:host{--font-sans:var(--font-sans);--font-mono:var(--font-mono);--spacing:var(--spacing);--container-xs:20rem;--container-5xl:64rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1Line truncated
|
||||
@@ -1 +1 @@
|
||||
"use strict";"require baseclass";"require ui";"require poll";"require rpc";const RT=window.L,RENDER_TIMEOUT=15e3,PATCH_ATTR="data-aurora-patch",NODE_CSS_ATTR="data-aurora-node-css",SHELL_ATTR="data-aurora-shell",PROGRESS_DELAY=150,INSTANTIATE=/instantiateView\(\s*['"]([^'"]+)['"]/,supported=()=>"object"==typeof navigation&&null!==navigation&&"function"==typeof navigation.addEventListener&&"function"==typeof NavigateEvent&&"intercept"in NavigateEvent.prototype,nodeWeight=e=>Math.min(e.order??9999,9999)+(e.auth?.login?1e4:0);function firstChild(e){let t=null;for(const[s,n]of Object.entries(e.children??{}))if(n.satisfied&&n.title&&"object"==typeof n.action)if("firstchild"===n.action?.type){if(t&&nodeWeight(t.node)<=nodeWeight(n))continue;firstChild(n)&&(t={name:s,node:n})}else n.firstchild_ineligible||(!t||nodeWeight(t.node)>nodeWeight(n))&&(t={name:s,node:n});return t}function resolve(e,t){let s=e,n=[],i=[],r=0;for(let o=0;o<32;o++){for(;r<t.length;){const e=s.children?.[t[r]];if(e&&!1===e.satisfied)return null;if(!e){if(!s.wildcard)return null;i=t.slice(r),r=t.length;break}s=e,n.push(t[r++])}const o=s.action?.type;if("alias"===o)t=[...String(s.action.path??"").split("/").filter(Boolean),...i],s=e,n=[],i=[],r=0;else{if("firstchild"!==o)return{node:s,path:n,args:i,request:t};{const e=firstChild(s);if(!e)return null;s=e.node,n.push(e.name)}}}return null}function readonlyAlong(e,t){const s=[];let n=e;for(const e of t)n=n?.children?.[e],n?.depends?.acl?.length&&s.push(n);return s.length>0&&s.every(e=>!0===e.readonly)}const actionOf=(e,t)=>t.length&&"object"==typeof e?.wildcardaction?e.wildcardaction:e?.action;function viewClass(e){return"view"===e?.type&&e.path?`view.${e.path.replace(/\//g,".")}`:null}const prefixes=e=>{const t=[];let s=null;for(const n of e)t.push(s=null==s?n:`${s}-${n}`);return t},PRAGMA_HEAD=/^(?:\s*["'](?:use strict|require\s[^"']+)["'];)+/,PRAGMA=/["']require\s+([^\s"']+)/g;function pragmaDeps(e){return[...(e.slice(0,4096).match(PRAGMA_HEAD)?.[0]??"").matchAll(PRAGMA)].map(e=>e[1])}const assetUrl=e=>{const t=document.body.dataset.assetVersion;return t?`${e}?v=${t}`:e},moduleUrl=e=>`${RT.env.base_url}/${e.replace(/\./g,"/")}.js${RT.env.resource_version?`?v=${RT.env.resource_version}`:""}`;return baseclass.extend({__init__(){if(!supported()||!RT?.env?.scriptname)return;const e=this.contract();e.length?console.warn(`router-aurora: luci-base surface missing, staying MPA: ${e.join(", ")}`):(this.gen=0,this.seen=new Set,this.warmed=new Set,this.inflight=Promise.resolve(),this.intervals=new Set,this.knownSheets=new WeakSet(this.sheets().filter(e=>e.hasAttribute(SHELL_ATTR)||e.hasAttribute(PATCH_ATTR)||e.hasAttribute(NODE_CSS_ATTR))),this.hostname=document.querySelector(".brand")?.textContent?.trim()||document.title,this.hookIntervals(),this.hookListeners(),this.hookSession(),this.hookVisibility(),Promise.all([ui.menu.load(),RT.require("menu-aurora")]).then(([e,t])=>{this.tree=e,this.menu=t;const s=this.initialRoute();s&&(document.querySelector('script[type="speculationrules"]')?.remove(),this.bar=document.body.appendChild(E("div",{id:"aurora-nav-progress","aria-hidden":"true"})),this.status=document.body.appendChild(E("div",{id:"aurora-nav-status",role:"status","aria-live":"polite","aria-atomic":"true"})),navigation.addEventListener("navigate",e=>this.onNavigate(e)),document.addEventListener("pointerover",e=>this.onIntent(e)),document.addEventListener("pointerdown",e=>this.onIntent(e)),document.addEventListener("focusin",e=>this.onIntent(e)),this.watchInitialRender(s))}))},contract(){const e={L:RT,rpc:rpc,poll:poll,ui:ui,window:window};return["L.view","L.require","L.dom.content","L.env.base_url","L.env.resource","L.env.media","L.Request.addInterceptor","L.uci.load","L.uci.unload","L.uci.state","rpc.addInterceptor","poll.queue","poll.start","poll.stop","poll.active","ui.menu.load","ui.hideModal","ui.hideIndicator","window.E"].filter(t=>null==(t=>t.split(".").reduce((e,t)=>null==e?e:e[t],e))(t))},hookVisibility(){document.addEventListener("visibilitychange",()=>{document.hidden?(this.pausedByVisibility=poll.active(),this.pausedByVisibility&&poll.stop()):this.pausedByVisibility&&(this.pausedByVisibility=!1,this.expired||poll.start())})},progressStart(){this.pending=(this.pending??0)+1,clearTimeout(this.progressTimer),this.progressTimer=setTimeout(()=>this.bar.dataset.state="active",150)},progressEnd(){--this.pending>0||(clearTimeout(this.progressTimer),"active"===this.bar.dataset.state&&(this.bar.dataset.state="done",this.progressTimer=setTimeout(()=>{"done"===this.bar.dataset.state&&delete this.bar.dataset.state},300)))},sheets(){const e=document.getElementById("view");return[...document.querySelectorAll('style, link[rel~="stylesheet"]')].filter(t=>!e?.contains(t))},poisoned(){return this.sheets().some(e=>!this.knownSheets.has(e)&&!e.hasAttribute(PATCH_ATTR)&&!e.hasAttribute(NODE_CSS_ATTR))},hookIntervals(){const e=window.setInterval,t=window.clearInterval,s=this.intervals;window.setInterval=functioLine truncated
|
||||
"use strict";"require baseclass";"require ui";"require poll";"require rpc";const RT=window.L,RENDER_TIMEOUT=15e3,PATCH_ATTR="data-aurora-patch",NODE_CSS_ATTR="data-aurora-node-css",SHELL_ATTR="data-aurora-shell",PROGRESS_DELAY=150,INSTANTIATE=/instantiateView\(\s*['"]([^'"]+)['"]/,supported=()=>"object"==typeof navigation&&null!==navigation&&"function"==typeof navigation.addEventListener&&"function"==typeof NavigateEvent&&"intercept"in NavigateEvent.prototype,nodeWeight=e=>Math.min(e.order??9999,9999)+(e.auth?.login?1e4:0);function firstChild(e){let t=null;for(const[s,n]of Object.entries(e.children??{}))if(n.satisfied&&n.title&&"object"==typeof n.action)if("firstchild"===n.action?.type){if(t&&nodeWeight(t.node)<=nodeWeight(n))continue;firstChild(n)&&(t={name:s,node:n})}else n.firstchild_ineligible||(!t||nodeWeight(t.node)>nodeWeight(n))&&(t={name:s,node:n});return t}function resolve(e,t){let s=e,n=[],i=[],r=0;for(let o=0;o<32;o++){for(;r<t.length;){const e=s.children?.[t[r]];if(e&&!1===e.satisfied)return null;if(!e){if(!s.wildcard)return null;i=t.slice(r),r=t.length;break}s=e,n.push(t[r++])}const o=s.action?.type;if("alias"===o)t=[...String(s.action.path??"").split("/").filter(Boolean),...i],s=e,n=[],i=[],r=0;else{if("firstchild"!==o)return{node:s,path:n,args:i,request:t};{const e=firstChild(s);if(!e)return null;s=e.node,n.push(e.name)}}}return null}function readonlyAlong(e,t){const s=[];let n=e;for(const e of t)n=n?.children?.[e],n?.depends?.acl?.length&&s.push(n);return s.length>0&&s.every(e=>!0===e.readonly)}const actionOf=(e,t)=>t.length&&"object"==typeof e?.wildcardaction?e.wildcardaction:e?.action;function viewClass(e){return"view"===e?.type&&e.path?`view.${e.path.replace(/\//g,".")}`:null}const prefixes=e=>{const t=[];let s=null;for(const n of e)t.push(s=null==s?n:`${s}-${n}`);return t},PRAGMA_HEAD=/^(?:\s*["'](?:use strict|require\s[^"']+)["'];)+/,PRAGMA=/["']require\s+([^\s"']+)/g;function pragmaDeps(e){return[...(e.slice(0,4096).match(PRAGMA_HEAD)?.[0]??"").matchAll(PRAGMA)].map(e=>e[1])}const assetUrl=e=>{const t=document.body.dataset.assetVersion;return t?`${e}?v=${t}`:e},moduleUrl=e=>`${RT.env.base_url}/${e.replace(/\./g,"/")}.js${RT.env.resource_version?`?v=${RT.env.resource_version}`:""}`;return baseclass.extend({__init__(){if(!supported()||!RT?.env?.scriptname)return;const e=this.contract();e.length?console.warn(`router-aurora: luci-base surface missing, staying MPA: ${e.join(", ")}`):(this.gen=0,this.seen=new Set,this.warmed=new Set,this.inflight=Promise.resolve(),this.intervals=new Set,this.knownSheets=new WeakSet(this.sheets().filter(e=>e.hasAttribute(SHELL_ATTR)||e.hasAttribute(PATCH_ATTR)||e.hasAttribute(NODE_CSS_ATTR))),this.hostname=document.querySelector(".brand")?.textContent?.trim()||document.title,this.hookIntervals(),this.hookListeners(),this.hookSession(),this.hookVisibility(),Promise.all([ui.menu.load(),RT.require("menu-aurora")]).then(([e,t])=>{this.tree=e,this.menu=t;const s=this.initialRoute();s&&(document.querySelector('script[type="speculationrules"]')?.remove(),this.status=document.body.appendChild(E("div",{id:"aurora-nav-status",role:"status","aria-live":"polite","aria-atomic":"true"})),navigation.addEventListener("navigate",e=>this.onNavigate(e)),document.addEventListener("pointerover",e=>this.onIntent(e)),document.addEventListener("pointerdown",e=>this.onIntent(e)),document.addEventListener("focusin",e=>this.onIntent(e)),this.watchInitialRender(s))}))},contract(){const e={L:RT,rpc:rpc,poll:poll,ui:ui,window:window};return["L.view","L.require","L.dom.content","L.env.base_url","L.env.resource","L.env.media","L.Request.addInterceptor","L.uci.load","L.uci.unload","L.uci.state","rpc.addInterceptor","poll.queue","poll.start","poll.stop","poll.active","ui.menu.load","ui.hideModal","ui.hideIndicator","window.E"].filter(t=>null==(t=>t.split(".").reduce((e,t)=>null==e?e:e[t],e))(t))},hookVisibility(){document.addEventListener("visibilitychange",()=>{document.hidden?(this.pausedByVisibility=poll.active(),this.pausedByVisibility&&poll.stop()):this.pausedByVisibility&&(this.pausedByVisibility=!1,this.expired||poll.start())})},progressStart(){this.pending=(this.pending??0)+1,clearTimeout(this.progressTimer),this.progressTimer=setTimeout(()=>{this.bar||(this.bar=document.body.appendChild(E("div",{id:"aurora-nav-progress","aria-hidden":"true"})),this.bar.offsetWidth,this.trickle(10))},150)},trickle(e){this.bar&&(this.bar.style.width=`${e}%`,this.trickleTimer=setTimeout(()=>this.trickle(e+(100-e)/30),300))},progressEnd(){if(--this.pending>0)return;clearTimeout(this.progressTimer),clearTimeout(this.trickleTimer);const e=this.bar;e&&(this.bar=null,e.dataset.state="done",e.style.width="100%",setTimeout(()=>e.remove(),300))},sheets(){const e=document.getElementById("view");return[...document.querySelectorAll('style, link[rel~="stylesheet"]')].filter(t=>!e?.contains(t))},poisoned(){return this.sheets().some(e=>!this.knownSheets.has(e)&&!e.hasAttribute(PATCH_ATTR)&&!e.hasAttribute(NODE_CSS_Line truncated
|
||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=21
|
||||
PKG_RELEASE:=22
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
'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
|
||||
@@ -278,43 +277,6 @@ 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',
|
||||
/* `noreferrer` alone, matching footer.ut's own two anchors: it implies noopener in every
|
||||
* engine that ships `rel=noreferrer` at all, so the pair reads as two rules where there is
|
||||
* one. fs-appearance.js's link to the same URL spells both — the two are now in step. */
|
||||
'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
|
||||
@@ -497,7 +459,6 @@ 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' */
|
||||
|
||||
@@ -42,14 +42,46 @@ function schedule() {
|
||||
requestAnimationFrame(() => { _rafPending = false; run(); });
|
||||
}
|
||||
|
||||
/* Watch an element's size. Any change re-fits everything — the fitters are cheap and few. */
|
||||
/* WIDTH ONLY, and this is not an optimisation — it is what makes the theme usable on a phone.
|
||||
*
|
||||
* Every browser on iOS grows and shrinks the viewport HEIGHT while the user scrolls, because the
|
||||
* URL bar slides away and comes back; each step of that animation is a resize, and a ResizeObserver
|
||||
* on #view reports it. Measured with the bar's travel simulated on a 390px viewport — twenty
|
||||
* height-only steps, width untouched — the fitters ran often enough to rewrite 1054 class
|
||||
* attributes, each one a forced synchronous layout of a page the user is scrolling. That is the
|
||||
* juddering reported from an iPhone.
|
||||
*
|
||||
* Nothing a fitter asks is about height: `roomFor()`/`overflows()` compare a table against its
|
||||
* column, `fitChrome()` asks whether the menu fits beside the brand, the rail and the density axes
|
||||
* change widths. A height-only change cannot alter any of those answers — and the one case that
|
||||
* looks like a counter-example is not one: a vertical scrollbar appearing takes WIDTH from the
|
||||
* content box, so the observer sees it as the width change it is.
|
||||
*
|
||||
* Per element, because the roots are observed separately and a dialog can resize while #view does
|
||||
* not. The first entry for an element always counts as a change, so nothing is lost at start-up. */
|
||||
const _lastWidth = new WeakMap();
|
||||
function onResize(entries) {
|
||||
let widthMoved = false;
|
||||
for (const e of entries) {
|
||||
/* contentRect, not getBoundingClientRect(): the observer already measured it, and asking
|
||||
* again inside the callback is the forced layout this function exists to avoid. */
|
||||
const w = Math.round(e.contentRect.width);
|
||||
if (_lastWidth.get(e.target) !== w) {
|
||||
_lastWidth.set(e.target, w);
|
||||
widthMoved = true;
|
||||
}
|
||||
}
|
||||
if (widthMoved) schedule();
|
||||
}
|
||||
|
||||
/* Watch an element's size. A change in WIDTH re-fits everything — the fitters are cheap and few. */
|
||||
function watch(el) {
|
||||
if (!el) return;
|
||||
/* No feature test: the shipped CSS needs :has() and container queries, both years younger than
|
||||
* ResizeObserver in every engine, so a browser that can render this theme at all has it. The
|
||||
* window-resize fallback that used to sit here was worse than nothing anyway — it cannot see a
|
||||
* rail collapse or a layout toggle, which is the pair this file uses an observer FOR. */
|
||||
if (!_ro) _ro = new ResizeObserver(schedule);
|
||||
if (!_ro) _ro = new ResizeObserver(onResize);
|
||||
_ro.observe(el);
|
||||
}
|
||||
|
||||
|
||||
@@ -120,21 +120,47 @@ function resolveSegs(segs) {
|
||||
* Save/Apply footer key their disabled state off, so on a session with narrower ACLs than root's
|
||||
* this is any page whose SECTION is read-only.
|
||||
*
|
||||
* OR down the path, exactly as check_acl_depends() folds the accumulated list: one read-only acl
|
||||
* anywhere on it is enough. Feed it the RESOLVED segments, since that is the path the dispatcher
|
||||
* accumulates over. */
|
||||
* AND down the path, not OR — the operator is the whole of this function and it was the wrong one.
|
||||
* check_acl_depends() (dispatcher.uc:312) is handed ONE list, the concatenation ctx_append() built
|
||||
* from every node's `depends.acl` along the path (:457-470), and it answers `writable = true` as
|
||||
* soon as ANY group in that list grants write. So a page is read-only exactly when NO group on the
|
||||
* path is writable — and since apply_tree_acls() (:436-446) marks a node `readonly` when that
|
||||
* node's own list yields no write, "no group on the path is writable" is "EVERY acl-bearing node on
|
||||
* the path is readonly". One read-only ancestor is not enough: a leaf that declares a writable acl
|
||||
* of its own re-opens the whole path.
|
||||
*
|
||||
* Measured, because a source reading is not a verdict: on the 25.12 stand, `admin/status/logs`
|
||||
* carries the read-only `luci-mod-status-logs` and its child `syslog` was given a writable acl of
|
||||
* its own; a full load then reported `nodespec.readonly` FALSE while this function said true — the
|
||||
* same class of disagreement between a click and an F5 that the leaf-only reading used to produce,
|
||||
* only in the opposite direction, and this time it TAKES AWAY a Save/Apply the server allows.
|
||||
*
|
||||
* A node with no `depends.acl` is not evidence either way and is skipped: the dispatcher puts
|
||||
* nothing into ctx.acls for it. That is also why the answer needs `depends.acl` at all rather than
|
||||
* the `readonly` flag alone — an acl-bearing node the session may write carries no flag, and is
|
||||
* therefore indistinguishable from an ungated node without looking at its acl list. /admin/menu
|
||||
* serves both fields (66 of 243 nodes carry an acl on the stand).
|
||||
*
|
||||
* Feed it the RESOLVED segments, since that is the path the dispatcher accumulates over. */
|
||||
function readonlyForSegs(segs) {
|
||||
let node = _tree;
|
||||
if (node && node.readonly === true)
|
||||
return true;
|
||||
let gated = 0, locked = 0;
|
||||
const weigh = (n) => {
|
||||
const acl = n && n.depends && n.depends.acl;
|
||||
if (!acl || !acl.length)
|
||||
return;
|
||||
gated++;
|
||||
if (n.readonly === true)
|
||||
locked++;
|
||||
};
|
||||
weigh(node);
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
node = node && node.children && node.children[segs[i]];
|
||||
if (!node)
|
||||
return false;
|
||||
if (node.readonly === true)
|
||||
return true;
|
||||
break;
|
||||
weigh(node);
|
||||
}
|
||||
return false;
|
||||
return gated > 0 && gated === locked;
|
||||
}
|
||||
|
||||
/* The view class a menu node instantiates, or null if the node isn't SPA-able. The Status→Overview
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require ui';
|
||||
'require rpc';
|
||||
'require fs-menutree as tree';
|
||||
'require fs-chrome as chrome';
|
||||
'require fs-sheets as sheets';
|
||||
@@ -64,11 +65,11 @@ function clearViewIntervals() {
|
||||
/* …and the ALIAS ITSELF is the first thing that can go — it is the deprecation the paragraph
|
||||
* above is about, so this function cannot be the one place that reads it blind. Every other
|
||||
* `L.Poll` read in this file is guarded, and an unguarded one here throws where a throw costs
|
||||
* most: navigate() calls this AFTER #view has been swapped for the "Loading view…" spinner and
|
||||
* BEFORE data-page, pushState and the require, so a TypeError would strand every click on the
|
||||
* spinner with the old URL and the old menu mark — far worse than the leaked interval this
|
||||
* exists to sweep. No alias is the same answer as an unreadable timer: do NOTHING, once,
|
||||
* loudly. */
|
||||
* most: navigate() calls this inside the staged render, after the chrome has already switched to
|
||||
* the incoming page and before its module is required, so a TypeError would leave every click
|
||||
* showing the previous page's content under the new page's title and menu mark, with the
|
||||
* navigation dead in a rejected promise — far worse than the leaked interval this exists to
|
||||
* sweep. No alias is the same answer as an unreadable timer: do NOTHING, once, loudly. */
|
||||
if (!L.Poll) {
|
||||
warnPollUnreadable('footstrap: L.Poll is gone from this luci-base, so LuCI\'s own tick cannot be '
|
||||
+ 'told apart from a view\'s timers — leaving view intervals alone. fs-router.js needs '
|
||||
@@ -202,6 +203,120 @@ function flushUciCache() {
|
||||
}
|
||||
let _uciCacheWarned = false;
|
||||
|
||||
/* ---- A DEAD SESSION ENDS THE DOCUMENT, AND THE ROUTER MUST NOT BROWSE THROUGH IT ----
|
||||
*
|
||||
* luci-base answers an expired session with `notifySessionExpiry()`: `Poll.stop()` plus a modal
|
||||
* whose only button reloads the page, which the dispatcher then answers with the login form. Every
|
||||
* navigation of ours does the opposite of both halves — `ui.hideModal()` and `L.Poll.stop()` +
|
||||
* `start()` — so the first click after the session died dismissed luci-base's own warning and
|
||||
* carried on. Measured on the stand before this existed: kill the session from inside the document,
|
||||
* let one rpc reject (`SessionError`, luci-base's modal up, polling stopped), then click a menu
|
||||
* link — the router swapped the view, the modal was gone, and the page sat on "Loading view…" with
|
||||
* every call behind it failing. Only a reload got the user back to a login form.
|
||||
*
|
||||
* So the router LEARNS that the session is gone and stops claiming navigations; the next click is a
|
||||
* real one and the dispatcher turns it into the login page. Nothing is reset — the flag dies with
|
||||
* the document, exactly as the session did.
|
||||
*
|
||||
* WHICH SIGNALS, and they are luci-base's own two decision points rather than a guess (luci.js,
|
||||
* `setupDOM`):
|
||||
*
|
||||
* 1. a `403` carrying `X-LuCI-Login-Required: yes` on any `L.Request` — upstream calls
|
||||
* `notifySessionExpiry()` on exactly that pair;
|
||||
* 2. the `session.access` probe luci-base fires after some other call came back `-32002`, when
|
||||
* that probe REJECTS — which is what upstream's `.catch(notifySessionExpiry)` reacts to.
|
||||
*
|
||||
* `access: false` is deliberately NOT one of them, and that is where this differs from the same
|
||||
* gate in luci-theme-aurora. The probe is declared `expect: { access: true }`, and rpc.js's
|
||||
* `handleCallReply()` turns a missing or mistyped key into the expectation's own value and
|
||||
* RESOLVES — so a probe answering `access: false` never reaches upstream's catch and never expires
|
||||
* anything. It is an ACL answer, and treating it as a dead session would drop a restricted user out
|
||||
* of the SPA for the rest of the document over a permission they simply do not have.
|
||||
*
|
||||
* The probe's rejection is read off the frame the interceptor is handed, because that is all an
|
||||
* interceptor can see: `handleCallReply()` rejects on a frame that is not JSON-RPC 2.0 or on an
|
||||
* `error` carrying both a code and a message, so those are the two conditions here. A foreign
|
||||
* `session.access` call of somebody else's that errors would also match — the cost is one document
|
||||
* of full loads, which is the safe direction to be wrong in.
|
||||
*
|
||||
* NEITHER INTERCEPTOR MAY THROW. luci-base runs both through `Promise.all(...).catch(req.reject)`
|
||||
* (`luci.js` for Request, `rpc.js` for rpc), so an exception in here would not just be our bug — it
|
||||
* would reject the caller's request, i.e. break the very page we are trying to protect. Hence the
|
||||
* try/catch around each body and no optional-chaining shortcuts on shapes we do not own. */
|
||||
let _expired = false;
|
||||
let _sessionWired = false;
|
||||
function markExpired() {
|
||||
if (_expired) return;
|
||||
_expired = true;
|
||||
/* once per document, and it is the console's least alarming level the house rules allow
|
||||
* (eslint no-console permits warn and error only): nothing is broken, the session simply
|
||||
* ended, and the very next click sends the user to a login form. */
|
||||
console.warn('footstrap: the LuCI session is gone — every navigation from here is a full load.');
|
||||
}
|
||||
function watchSession() {
|
||||
if (_sessionWired) return;
|
||||
_sessionWired = true;
|
||||
|
||||
const req = window.L ? window.L.Request : null;
|
||||
if (req && typeof req.addInterceptor === 'function')
|
||||
req.addInterceptor((res) => {
|
||||
try {
|
||||
if (res && res.status === 403 && res.headers &&
|
||||
res.headers.get('X-LuCI-Login-Required') === 'yes')
|
||||
markExpired();
|
||||
}
|
||||
catch (e) { /* see above: an interceptor that throws rejects the caller's request */ }
|
||||
});
|
||||
|
||||
if (rpc && typeof rpc.addInterceptor === 'function')
|
||||
rpc.addInterceptor((msg, r) => {
|
||||
try {
|
||||
if (!r || r.object !== 'session' || r.method !== 'access') return;
|
||||
if (!msg || msg.jsonrpc !== '2.0' ||
|
||||
(msg.error && msg.error.code && msg.error.message))
|
||||
markExpired();
|
||||
}
|
||||
catch (e) { /* ditto */ }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- throw an element away the way luci-base throws one away ----
|
||||
*
|
||||
* `dom.data()` does not live on the element: luci.js keeps it in `dom.registry`, keyed by a
|
||||
* `data-idref` attribute, and the ONE thing that ever deletes an entry is `dom.content()`
|
||||
* (luci.js: it walks `[data-idref]` and deletes each key before emptying the node). A plain
|
||||
* `remove()` therefore leaves the entry — and through it the element and whatever class instance
|
||||
* was stored on it — reachable for the life of the document.
|
||||
*
|
||||
* `#view` is not affected, and that is why this is small: the incoming view's own
|
||||
* `dom.content(#view, …)` reaps the outgoing page. What the router removes BY HAND is the rest —
|
||||
* the siblings a template emitted next to `#view`, and the runtime notification banners — and those
|
||||
* it used to drop with `remove()`.
|
||||
*
|
||||
* MEASURED, before writing it: over three pages on the 25.12 stand nothing the sweeps remove
|
||||
* carries a `data-idref` at all (banners: 0, siblings: 0), and the registry does not grow across
|
||||
* laps (83 entries after the first lap of four pages, 83 after the third). So this fixes no leak we
|
||||
* can see today; it closes the class, for the same reason the stackables selector claims a table
|
||||
* with no LuCI classes — coverage is a contract, and the alternative is discovering it from a
|
||||
* third-party page nobody here runs.
|
||||
*
|
||||
* The bin is what makes the element's OWN entry go too: `dom.content()` reaps descendants of the
|
||||
* node it is given, never the node itself, so the element is moved into a detached container first
|
||||
* — which also takes it out of the live tree, i.e. does the removal. Public API only; no reaching
|
||||
* into `dom.registry`. If luci-base ever moves that surface, the fallback is today's behaviour. */
|
||||
function discard(el) {
|
||||
try {
|
||||
const dom = window.L ? window.L.dom : null;
|
||||
if (!dom || typeof dom.content !== 'function') { el.remove(); return; }
|
||||
const bin = document.createElement('div');
|
||||
bin.appendChild(el);
|
||||
dom.content(bin, null);
|
||||
}
|
||||
catch (e) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
let _wired = false;
|
||||
/* The pathname whose view is CURRENTLY rendered — popstate compares against it to tell a real
|
||||
* navigation from a mere fragment change (see there). Seeded from the served page. */
|
||||
@@ -244,6 +359,9 @@ let _curId = null;
|
||||
* goes first: a Map iterates in insertion order and `set` on an existing key does not refresh it,
|
||||
* so the re-save below is delete-then-set. */
|
||||
const SCROLL_MEM_MAX = 50;
|
||||
/* The offset a popstate replay must land on, consumed by the commit that puts the page on screen —
|
||||
* see the popstate handler for why it cannot be replayed any earlier. */
|
||||
let _pendingRestore = null;
|
||||
function newEntryId() { return _scrollSess + ':' + (++_histN); }
|
||||
|
||||
/* adopt the entry we are standing on: reuse its fsid if it has one, stamp one otherwise (entries
|
||||
@@ -354,89 +472,6 @@ function titleHost() {
|
||||
return _titleHost;
|
||||
}
|
||||
|
||||
/* The view class the page CURRENTLY on screen wants (what _curPath resolves to). Read by the
|
||||
* stale-render repair below to tell "the superseded render happened to paint the right view
|
||||
* anyway" from "it painted the wrong one". */
|
||||
function currentViewClass() {
|
||||
const segs = tree.segsFromPath(_curPath);
|
||||
const res = segs && tree.resolveSegs(segs);
|
||||
return tree.viewClassFor(res && res.node);
|
||||
}
|
||||
|
||||
/* ---- a superseded FIRST render cannot be cancelled, so undo it ----
|
||||
*
|
||||
* _navGen stops a stale require() from calling `new view.constructor()` — but only on the CACHED
|
||||
* path. On a FIRST visit the require() IS the render (see navigate()): it constructs the view, whose
|
||||
* __init__ runs load() → render() → dom.content(#view) and registers its pollers, inside a promise
|
||||
* we do not own. Nothing to cancel.
|
||||
*
|
||||
* So the fast double-click is a real bug: click Firewall (uncached), click Wireless 100 ms later.
|
||||
* navigate(Wireless) flushes L.Poll's queue BEFORE Firewall's poller is added; Firewall then paints
|
||||
* into the #view that now belongs to Wireless and registers a poller the flush can no longer catch —
|
||||
* leaving Wireless's URL/title/menu/data-page, Firewall's content, and Firewall's poller running on
|
||||
* every page afterwards.
|
||||
*
|
||||
* Repair by re-running the current navigation: navigate() is exactly the "put the document back the
|
||||
* way a fresh load leaves it" routine. push=false — the URL never moved, only the DOM under it; if
|
||||
* it declines (the superseded view injected CSS), the reload does it the hard way. The className
|
||||
* check terminates this: if the superseded render painted the class the current path wants anyway
|
||||
* (A → B → A while A was still loading), the DOM and its poller are correct — and with two uncached
|
||||
* views racing it is also what stops a repair triggering a repair. */
|
||||
function repairStaleRender(className) {
|
||||
if (className === currentViewClass())
|
||||
return;
|
||||
console.warn('footstrap: a superseded view (' + className + ') rendered into the live page; re-rendering ' + _curPath);
|
||||
if (!navigate(_curPath, false))
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/* ---- the generation must be checked at the PAINT, not at the dispatch ----
|
||||
*
|
||||
* _navGen is checked once, in the require() callback, i.e. BEFORE `new view.constructor()`. But a
|
||||
* view's __init__ is async — `ready.then(this.load).then(this.render).then(nodes =>
|
||||
* dom.content(document.getElementById('view'), nodes))` — so the DOM write happens two awaits later,
|
||||
* and every await is a point at which the whole event loop runs, another navigation included. The
|
||||
* dispatch-time check has expired by the time it matters.
|
||||
*
|
||||
* Measured on the router: leave the (cached) package manager for System after 150 ms, and the paints
|
||||
* into #view land 16010 System, 16490 package-manager — the view we walked away from paints LAST and
|
||||
* wins, permanently. URL, <title>, data-page and the menu highlight all say System; #view shows
|
||||
* Software. Only a reload clears it. The cached path is the COMMON one — after warm-up every
|
||||
* navigation takes it — and it had no guard at all: repairStaleRender() is called only when !cached.
|
||||
*
|
||||
* The fix has to be a paint-time check, and there are two constraints on where it can live.
|
||||
* ClassConstructor DISCARDS __init__'s return value (`this.__init__.apply(this, arguments)`, no
|
||||
* return), so the construction promise is unreachable — nothing to await. But __init__ resolves
|
||||
* `this.render` while it builds its chain, i.e. during `new`, so a wrapper installed on the
|
||||
* prototype BEFORE the construct is the one that gets bound. `new` then returns synchronously (the
|
||||
* first load() is already a microtask), which is what makes stamping the generation on the INSTANCE
|
||||
* right after it safe — and it must be the instance, not a map keyed by class name, or A → B → A
|
||||
* with everything cached would have the second construct's generation overwrite the first's.
|
||||
*
|
||||
* A superseded render resolves to a promise that never settles: the chain simply stops before
|
||||
* dom.content(), so nothing paints and addFooter() never runs. Returning empty nodes instead would
|
||||
* paint the emptiness over the live page, and throwing would hand LuCI's own .catch an error box to
|
||||
* render into the page we just opened.
|
||||
*
|
||||
* A view instance we did not construct (the singleton require() builds on a FIRST visit — the render
|
||||
* IS the require, so there is no window in which to arm anything) carries no stamp and is left alone:
|
||||
* that path is repairStaleRender()'s, and stays its. */
|
||||
const _guarded = new WeakSet();
|
||||
function armRenderGuard(cls) {
|
||||
if (_guarded.has(cls)) return;
|
||||
_guarded.add(cls);
|
||||
const orig = cls.prototype.render;
|
||||
cls.prototype.render = function () {
|
||||
/* unstamped: not ours to judge (see above) */
|
||||
const stale = () => this.__fsGen !== undefined && this.__fsGen !== _navGen;
|
||||
if (stale())
|
||||
return new Promise(() => {});
|
||||
return Promise.resolve(orig.apply(this, arguments)).then((nodes) => {
|
||||
/* re-check: render() itself awaits (an RPC, usually), which re-opens the window */
|
||||
return stale() ? new Promise(() => {}) : nodes;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/* The exact URL LuCI.require() will fetch for a class name, cache-bust and all. Matching it
|
||||
* byte-for-byte is what makes a hover prefetch a warm cache hit for the later require(). */
|
||||
@@ -582,6 +617,223 @@ function seed() {
|
||||
titleHost(); /* before any view can rename the tab — see there */
|
||||
/* the served page's entry needs an id too, or the first Back TO it has nothing to look up */
|
||||
adoptEntry();
|
||||
|
||||
/* ---- THE DOCUMENT'S OWN FIRST RENDER IS THE FIRST LINK IN THE CHAIN ----
|
||||
*
|
||||
* A navigation waits for the previous render because a LuCI view chain resolves `#view` at paint
|
||||
* time and would otherwise paint into the newer navigation's stage. The very first chain in a
|
||||
* document is not one of ours — `view.ut`'s inline `instantiateView()` is already running when
|
||||
* the chrome comes up — and it is subject to exactly the same rule. Left untracked it was a real
|
||||
* bug, not a theoretical one: measured at 350 ms latency, a click 150 ms after DOMContentLoaded
|
||||
* ended with the URL, title and menu on System and the Processes the document had been loading
|
||||
* painted over it, because that chain finished last and found the stage first in tree order.
|
||||
*
|
||||
* So the chain starts here, watching the live `#view` with the same observer a staged render
|
||||
* uses. If the first view has already painted, `renderedIn()` resolves at once and the first
|
||||
* click waits for nothing. The `.catch` is what keeps a document whose first view never renders
|
||||
* (an error page, a dead RPC) from turning every later click into a rejected promise — and from
|
||||
* logging an unhandled rejection at the 15 s mark on a page nobody clicked. */
|
||||
const vp = document.getElementById('view');
|
||||
if (vp) _inflight = renderedIn(vp).catch(() => {});
|
||||
}
|
||||
|
||||
/* ---- THE INCOMING PAGE IS RENDERED OFF SCREEN AND SWAPPED IN WHEN IT IS READY ----
|
||||
*
|
||||
* What this replaces: the router used to empty `#view`, paint luci-base's "Loading view…" spinner
|
||||
* on a cold route and let the incoming view render into the live page. Two things came with that.
|
||||
* The user watched an empty page for as long as the module and its data took — measured at 600 ms
|
||||
* latency, 1800 ms of nothing on a first visit — and a superseded render could not be stopped, so
|
||||
* the file carried three mechanisms to repair the damage afterwards: a generation stamped on the
|
||||
* instance, a wrapper around `prototype.render` that made a stale paint hang forever, and a
|
||||
* re-navigation that undid a stale FIRST render after the fact.
|
||||
*
|
||||
* Rendering into a stage removes the damage instead of repairing it. `stageView()` puts a fresh
|
||||
* `<div id="view">` inside a hidden wrapper as the FIRST child of `.fs-content`, and
|
||||
* `getElementById` returns the first match in tree order — which is what LuCI's own view chain
|
||||
* calls, twice: once in `View.__init__` for the spinner and again when the render resolves
|
||||
* (`DOM.content(document.getElementById('view'), nodes)`). So the incoming view writes into the
|
||||
* stage while the page the user is reading stays on screen, untouched, until the swap.
|
||||
*
|
||||
* HIDDEN, BUT LAID OUT. `visibility: hidden; height: 0; overflow: clip` — never `display: none`.
|
||||
* Several views size themselves from the element they render into, and they find it the same way
|
||||
* LuCI does: `view/status/load.js` does `document.querySelector('#view')` and then
|
||||
* `view.offsetWidth - 2` (bandwidth.js, connections.js and wireless.js repeat it verbatim), so a
|
||||
* `display: none` stage would hand them a zero width they keep for the life of the instance. The
|
||||
* stage's width is the container's, exactly what it will be after the swap — measured rather than
|
||||
* assumed: the Load graph comes out 1222px wide whether the page is reached by a full load or by a
|
||||
* click. Only the height is taken away, so the page under it does not move.
|
||||
*
|
||||
* AND THE SWAP MOVES THE NODES, IT DOES NOT SWAP THE ELEMENT. The other obvious shape — insert the
|
||||
* staged `#view` and delete the old one — would change the identity of `#view`, and this theme has
|
||||
* observers bound to that element: `fs-fit`'s content MutationObserver and `fs-appearance`'s view
|
||||
* observer are registered on the node that existed at chrome init. Swapping the element leaves both
|
||||
* watching a detached node, i.e. the fitters silently stop re-running on content mutations. So the
|
||||
* live `#view` keeps its identity and its children are replaced through `dom.content()`, which is
|
||||
* also what reaps the outgoing page's `data-idref` registry entries.
|
||||
*
|
||||
* RENDERS ARE SERIALIZED, and this is what actually retires the repair machinery. Neither an
|
||||
* in-flight LuCI XHR nor a running `View.__init__` chain can be cancelled — `L.Request` never
|
||||
* exposes its handle — and every chain resolves `#view` at PAINT time, so a chain started by an
|
||||
* older navigation would paint into whatever stage is first when it finishes, i.e. into the newer
|
||||
* navigation's. A navigation therefore waits for the previous one to finish before it stages
|
||||
* anything; the older chain paints into its own stage, which is then dropped unswapped. The cost is
|
||||
* stated plainly: a click during a slow first load waits for that load. It buys the deletion of
|
||||
* three mechanisms that only ever cleaned up afterwards.
|
||||
*
|
||||
* COMPLETION IS OBSERVED, NOT ASSUMED. `renderedIn()` watches the stage and resolves when a child
|
||||
* that is not the spinner appears — or when a mutation leaves it empty, which is how a view that
|
||||
* renders nothing at all finishes. A render that has not completed within RENDER_TIMEOUT is a
|
||||
* FAILURE, not a completion: swapping a spinner in and releasing the serialization would let the
|
||||
* still-running chain paint into a later navigation's stage, so it rejects into the same full-load
|
||||
* fallback every other error takes. */
|
||||
const RENDER_TIMEOUT = 15000;
|
||||
/* the promise of the render currently in flight — this initial value only covers a document whose
|
||||
* chrome came up without seed() having run; seed() replaces it with the document's own first render */
|
||||
let _inflight = Promise.resolve();
|
||||
|
||||
function stageView(contentHost) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'fs-staging';
|
||||
const view = document.createElement('div');
|
||||
view.id = 'view';
|
||||
wrapper.appendChild(view);
|
||||
/* FIRST in tree order, or getElementById() would keep answering with the live one */
|
||||
contentHost.insertBefore(wrapper, contentHost.firstChild);
|
||||
return { wrapper, view };
|
||||
}
|
||||
|
||||
function renderedIn(view) {
|
||||
/* `.spinning` is luci-base's own placeholder, written by View.__init__ before load() runs;
|
||||
* a <script> is what a template shell replays. Neither is the page. */
|
||||
const painted = () => view.querySelector(':scope > :not(.spinning):not(script)') !== null;
|
||||
if (painted()) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = window.setTimeout(() => {
|
||||
finish(() => reject(new Error('the view did not render within ' + RENDER_TIMEOUT + ' ms')));
|
||||
}, RENDER_TIMEOUT);
|
||||
const mo = new MutationObserver(() => {
|
||||
/* an empty render finishes too: the spinner is replaced by nothing at all, which is a
|
||||
* mutation that leaves no element children behind. No "has it started" flag guards this:
|
||||
* `painted()` was asked before the observer existed, and an observer only ever fires on a
|
||||
* mutation, so an empty stage cannot resolve before the render has touched it. */
|
||||
if (painted() || view.childElementCount === 0) finish(resolve);
|
||||
});
|
||||
function finish(settle) {
|
||||
window.clearTimeout(timer);
|
||||
mo.disconnect();
|
||||
settle();
|
||||
}
|
||||
mo.observe(view, { childList: true });
|
||||
});
|
||||
}
|
||||
|
||||
function dropStage(stage) {
|
||||
if (stage && stage.wrapper && stage.wrapper.parentNode) discard(stage.wrapper);
|
||||
}
|
||||
|
||||
/* Put the staged page on screen: clear what the outgoing page left beside `#view`, move the staged
|
||||
* children into the live `#view` through `dom.content()`, and drop the wrapper. */
|
||||
function commitStage(stage, contentHost) {
|
||||
/* the outgoing page is going off screen now, so the sheets it owned may finally be darkened —
|
||||
* the half navigate() spared while it was still being read */
|
||||
sheets.scopeToCurrentPage();
|
||||
sweepAround(contentHost);
|
||||
const live = liveView(contentHost, stage);
|
||||
const nodes = Array.from(stage.view.childNodes);
|
||||
const dom = window.L ? window.L.dom : null;
|
||||
if (live && dom && typeof dom.content === 'function')
|
||||
dom.content(live, nodes);
|
||||
else if (live)
|
||||
live.replaceChildren(...nodes);
|
||||
dropStage(stage);
|
||||
}
|
||||
|
||||
/* The `#view` the document keeps between navigations — the one the observers are bound to. It is
|
||||
* whichever `#view` is NOT the stage; a document that has none (a template page that emitted no
|
||||
* `#view` at all) gets one, once. */
|
||||
function liveView(contentHost, stage) {
|
||||
for (const el of contentHost.querySelectorAll(':scope > #view'))
|
||||
if (el !== stage.view) return el;
|
||||
const v = document.createElement('div');
|
||||
v.id = 'view';
|
||||
contentHost.appendChild(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Clear what the OUTGOING page left as a SIBLING of #view inside .fs-content: dom.content()
|
||||
* replaces only #view's OWN children, so anything a page emitted next to it rides along — the
|
||||
* Status→Overview template emits <h2 name="content">Status</h2> there, hidden only by a
|
||||
* body[data-page='admin-status-overview'] rule, so after an SPA nav the orphan showed on EVERY page
|
||||
* until a full reload. Keep only the chrome that legitimately outlives a page (tabs, server
|
||||
* notices, <noscript>) and the stage itself.
|
||||
*
|
||||
* …and the RUNTIME notifications, which live one level up and therefore survived every navigation.
|
||||
* `ui.addNotification()` does `mc.insertBefore(msg, mc.firstElementChild)` on #maincontent — this
|
||||
* theme's <main class="fs-main"> — while the sweep above only reaches children of .fs-content, a
|
||||
* descendant of it. A full load clears them; SPA never did, so "Upload request failed" and every
|
||||
* third-party banner stacked up over each following page for the rest of the session. The
|
||||
* .fs-content banners kept above are the SERVER's notices (notices.ut) and legitimately outlive a
|
||||
* page; these are not the same thing. */
|
||||
function sweepAround(contentHost) {
|
||||
Array.from(contentHost.children).forEach((c) => {
|
||||
if (c.id !== 'view' && c.id !== 'tabmenu' && !c.classList.contains('fs-staging') &&
|
||||
!c.classList.contains('alert-message') && c.nodeName !== 'NOSCRIPT')
|
||||
discard(c);
|
||||
});
|
||||
const mainHost = document.getElementById('maincontent');
|
||||
if (mainHost)
|
||||
Array.from(mainHost.children).forEach((c) => {
|
||||
if (c.classList.contains('alert-message')) discard(c);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- and something has to say that a slow navigation IS a navigation ----
|
||||
*
|
||||
* With the outgoing page left on screen until the incoming one is ready, a cold route would
|
||||
* otherwise look like a click that did nothing: the chrome switches instantly (title, URL, menu),
|
||||
* the content does not move for as long as the module and its first RPC take. The spinner this
|
||||
* replaced was worse — it threw the page away to say the same thing — but silence is not the
|
||||
* answer either.
|
||||
*
|
||||
* A hairline at the top of the content, shown only when the navigation OUTLIVES `PROGRESS_DELAY`.
|
||||
* Below that threshold a bar would flash on and off on every warm click, which reads as a glitch
|
||||
* rather than as progress; above it, it is the browser's own affordance for exactly this. The
|
||||
* counter is what makes overlapping navigations share one bar. Reduced motion is handled in CSS,
|
||||
* where the transitions live. */
|
||||
const PROGRESS_DELAY = 150;
|
||||
let _progressPending = 0;
|
||||
let _progressTimer = 0;
|
||||
function progressBar() {
|
||||
let bar = document.getElementById('fs-nav-progress');
|
||||
if (!bar) {
|
||||
bar = document.createElement('div');
|
||||
bar.id = 'fs-nav-progress';
|
||||
/* it says nothing a screen reader needs: the live region already announces the page, and a
|
||||
* decorative bar in the accessibility tree is noise on every navigation */
|
||||
bar.setAttribute('aria-hidden', 'true');
|
||||
/* ON <body>, not in the content column: it is `position: fixed`, and `.fs-shell` carries
|
||||
* `contain: paint` in the sidebar layout, which would make it position against the shell and
|
||||
* clip it. Inside `.fs-main` — a column flex container — its 2px were shrunk to zero
|
||||
* outright. Both measured; see styles/theme/20-shell.css. */
|
||||
document.body.insertBefore(bar, document.body.firstChild);
|
||||
}
|
||||
return bar;
|
||||
}
|
||||
function progressStart() {
|
||||
_progressPending++;
|
||||
window.clearTimeout(_progressTimer);
|
||||
_progressTimer = window.setTimeout(() => { progressBar().dataset.state = 'active'; }, PROGRESS_DELAY);
|
||||
}
|
||||
function progressEnd() {
|
||||
if (--_progressPending > 0) return;
|
||||
_progressPending = 0;
|
||||
window.clearTimeout(_progressTimer);
|
||||
const bar = progressBar();
|
||||
if (bar.dataset.state !== 'active') return;
|
||||
bar.dataset.state = 'done';
|
||||
_progressTimer = window.setTimeout(() => {
|
||||
if (bar.dataset.state === 'done') delete bar.dataset.state;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
/* Attempt an in-place navigation to `pathname`. Returns true if handled as a
|
||||
@@ -596,6 +848,11 @@ function navigate(pathname, push, kbd) {
|
||||
* the only exit that leaves BOTH pages correct is a real navigation. See fs-sheets.js. */
|
||||
if (sheets.documentPoisoned()) return false;
|
||||
|
||||
/* …and a document whose session has died is spent in the same way, for a different reason: the
|
||||
* only page it can still render correctly is the login form, and only a real navigation gets
|
||||
* there. See watchSession(). */
|
||||
if (_expired) return false;
|
||||
|
||||
/* `segs` is what the user clicked, `rsegs` the leaf it resolves to; they differ for an
|
||||
* alias/firstchild link, and a full load keeps BOTH — URL and pathinfo as requested,
|
||||
* requestpath/dispatchpath/nodespec/title resolved. Mirror that split exactly, or an F5
|
||||
@@ -641,94 +898,24 @@ function navigate(pathname, push, kbd) {
|
||||
|
||||
/* from here on the navigation is committed */
|
||||
const gen = ++_navGen;
|
||||
/* THE SAVED OFFSET BELONGS TO THIS NAVIGATION, so it is taken out of the module slot here
|
||||
* rather than read at the swap. Left in the slot it outlives its own navigation: Back sets it,
|
||||
* a click supersedes that render before it commits, and the clicked page — a page the user
|
||||
* never asked to be restored — is scrolled to the Back target's offset, under a generation that
|
||||
* passes because it is the newer one's. Clearing it on the superseded path is not the answer
|
||||
* either: a second popstate may legitimately have put ITS offset there by then. Reported in
|
||||
* review. */
|
||||
const restoreTo = _pendingRestore;
|
||||
_pendingRestore = null;
|
||||
_curPath = pathname; /* what is on screen from now on — read by the popstate handler */
|
||||
|
||||
/* Ensure a #view, and clear what the OUTGOING page left as a SIBLING of #view inside .fs-content:
|
||||
* dom.content() replaces only #view's OWN children, so anything a page emitted next to it rides
|
||||
* along — the Status→Overview template emits <h2 name="content">Status</h2> there, hidden only
|
||||
* by a body[data-page='admin-status-overview'] rule, so after an SPA nav the orphan showed on
|
||||
* EVERY page until a full reload. Keep only the chrome that legitimately outlives a page (tabs,
|
||||
* server notices, <noscript>); this also gives a template page that emits no #view a fresh one. */
|
||||
const contentHost = document.querySelector('.fs-content');
|
||||
if (!contentHost) return false;
|
||||
Array.from(contentHost.children).forEach((c) => {
|
||||
if (c.id !== 'view' && c.id !== 'tabmenu' &&
|
||||
!c.classList.contains('alert-message') && c.nodeName !== 'NOSCRIPT')
|
||||
c.remove();
|
||||
});
|
||||
/* …and the RUNTIME notifications, which live one level up and therefore survived every
|
||||
* navigation. `ui.addNotification()` does `mc.insertBefore(msg, mc.firstElementChild)` on
|
||||
* #maincontent — this theme's <main class="fs-main"> — while the sweep above only reaches
|
||||
* children of .fs-content, a descendant of it. A full load clears them; SPA never did, so
|
||||
* "Upload request failed" and every third-party banner stacked up over each following page for
|
||||
* the rest of the session (reproduced: the banner outlives a nav, and only F5 removes it).
|
||||
* The .fs-content banners kept above are the SERVER's notices (notices.ut) and legitimately
|
||||
* outlive a page; these are not the same thing. */
|
||||
const mainHost = document.getElementById('maincontent');
|
||||
if (mainHost)
|
||||
Array.from(mainHost.children).forEach((c) => {
|
||||
if (c.classList.contains('alert-message')) c.remove();
|
||||
});
|
||||
if (!document.getElementById('view')) {
|
||||
const v = document.createElement('div');
|
||||
v.id = 'view';
|
||||
contentHost.appendChild(v);
|
||||
}
|
||||
|
||||
/* SAY THAT SOMETHING IS LOADING, on a route whose module has never been fetched.
|
||||
*
|
||||
* The chrome switches instantly — title, URL, body[data-page], the menu highlight — but the
|
||||
* sweep above deliberately does not touch the children of #view, and LuCI's own spinner is
|
||||
* written by `View.__init__`, i.e. only once the view module has ARRIVED. So between the click
|
||||
* and the last round-trip of the module chain the user reads the PREVIOUS page's content under
|
||||
* the new page's title, with nothing moving. Measured on the router at 600 ms latency: at 150 ms
|
||||
* and 400 ms #view still held the System page while everything else said Processes; the
|
||||
* "Loading view…" spinner appeared at 900 ms and the content at 1800 ms. A full load would have
|
||||
* shown the browser's own progress for that whole window.
|
||||
*
|
||||
* Only for a COLD route: `_seen` means the class has already been through require(), where
|
||||
* View.__init__ paints its spinner synchronously and there is no gap to fill.
|
||||
*
|
||||
* Deliberately the SAME markup and the SAME msgid luci-base uses, not a skeleton of our own:
|
||||
* when View.__init__ replaces it there is nothing to see, and the string arrives already
|
||||
* translated in the ~40 languages this theme ships no catalogue for (the chrome rule — a
|
||||
* context-free msgid is how we inherit luci-base's translation). */
|
||||
if (!_seen.has(className)) {
|
||||
const vp = document.getElementById('view');
|
||||
if (vp) {
|
||||
const s = document.createElement('div');
|
||||
s.className = 'spinning';
|
||||
s.textContent = _('Loading view…');
|
||||
vp.replaceChildren(s);
|
||||
}
|
||||
}
|
||||
/* the page being LEFT, captured before L.env is re-pointed a few lines down: the sheet scoping
|
||||
* spares it until the swap takes it off screen (see there) */
|
||||
const leaving = (L.env.dispatchpath || []).slice();
|
||||
|
||||
/* teardown: drop the outgoing view's pollers, then put the poll loop back into the state a FRESH
|
||||
* LOAD leaves it in. The only non-view poller LuCI adds is the transient apply/reboot
|
||||
* reachability check, so flushing the queue is safe.
|
||||
*
|
||||
* The re-arm matters: LuCI runs one 1 s tick and fires a queue entry only when
|
||||
* `tick % interval == 0`, so leaving the OUTGOING page's tick running makes the incoming poller
|
||||
* wait for the next multiple of its interval — up to `pollinterval`, 5 s. Wireless draws its
|
||||
* station list from the first poll and sat spinning for 4950 ms against ~360 ms on a full load.
|
||||
*
|
||||
* stop() alone is NOT the fix: it deletes `tick`, and Poll.add() only auto-starts when
|
||||
* `tick != null`, so the incoming pollers would never start at all. stop()+start() on an EMPTY
|
||||
* queue leaves what a fresh document has (`tick = 0`, no timer armed); the view's first
|
||||
* poll.add() then starts it and steps immediately — upstream's own sequence, since on a full
|
||||
* load initDOM() runs Poll.start() on an empty queue before the view renders. */
|
||||
if (L.Poll && L.Poll.queue) {
|
||||
L.Poll.queue.length = 0;
|
||||
L.Poll.stop();
|
||||
L.Poll.start();
|
||||
}
|
||||
/* kill the outgoing view's plain setInterval pollers too (podkop's log tailer) — a full load
|
||||
* 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). 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
|
||||
@@ -769,21 +956,6 @@ function navigate(pathname, push, kbd) {
|
||||
L.env.nodespec = { satisfied: true, action: node.action, title: node.title,
|
||||
depends: node.depends, readonly: tree.readonlyForSegs(rsegs) };
|
||||
|
||||
/* Keep <body data-page> in sync with the route: the server stamps the dispatch path
|
||||
* (`ctx.path`) on every full load, and page-scoped CSS keys off it. `rsegs` is the RESOLVED
|
||||
* leaf, so a firstchild URL like /admin/status yields the same "admin-status-overview" whether
|
||||
* it arrives as a full load or a client nav. Without the re-stamp the incoming page keeps the
|
||||
* previous page's data-page and its scoped styles silently do not apply. */
|
||||
document.body.setAttribute('data-page', rsegs.join('-'));
|
||||
|
||||
/* …and immediately hand the new page to fs-sheets, which darkens every foreign sheet that
|
||||
* belongs to a DIFFERENT page and re-lights the ones that belong to this one. This is what lets
|
||||
* an invasive sheet stay in the document without spending it — the alternative was a full load
|
||||
* on the way out of any page carrying one, which stock LuCI's five realtime views all do (see
|
||||
* the page-ownership block in fs-sheets.js). It must run AFTER the stamp above and BEFORE the
|
||||
* view renders, so nothing paints through a sheet that no longer owns the page. */
|
||||
sheets.scopeToCurrentPage(rsegs);
|
||||
|
||||
/* Re-navigating to the page already on screen must REPLACE its history entry, not push a
|
||||
* second one. Clicking the active menu item is ordinary, and a duplicate entry makes Back do
|
||||
* nothing: popstate fires, `location.pathname === _curPath`, and the fragment guard below
|
||||
@@ -870,60 +1042,134 @@ 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. */
|
||||
/* `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(() => {
|
||||
/* THE PREVIOUS RENDER IS WAITED FOR, not raced. Every LuCI view chain resolves `#view` at PAINT
|
||||
* time, so a chain still running from an older navigation would paint into whatever stage is
|
||||
* first when it finishes — i.e. into this one's. Waiting is what lets the stale chain land in
|
||||
* its own stage, which is then dropped unswapped, and it is what retired the three repair
|
||||
* mechanisms this file used to carry. The wait rides alongside the prefetch wait rather than
|
||||
* after it: both are already in flight. */
|
||||
const previous = _inflight;
|
||||
let release;
|
||||
_inflight = new Promise((r) => { release = r; });
|
||||
progressStart();
|
||||
|
||||
Promise.all([ warmedThen(className), previous.catch(() => {}) ]).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. */
|
||||
* render, so starting it here would spend a module fetch and a round of RPCs on a page the
|
||||
* user has already left. */
|
||||
if (gen !== _navGen) return null;
|
||||
|
||||
/* ---- teardown, now that the previous render is finished and cannot re-register ----
|
||||
*
|
||||
* Drop the outgoing view's pollers, then put the poll loop back into the state a FRESH LOAD
|
||||
* leaves it in. The only non-view poller LuCI adds is the transient apply/reboot
|
||||
* reachability check, so flushing the queue is safe.
|
||||
*
|
||||
* The re-arm matters: LuCI runs one 1 s tick and fires a queue entry only when
|
||||
* `tick % interval == 0`, so leaving the OUTGOING page's tick running makes the incoming
|
||||
* poller wait for the next multiple of its interval — up to `pollinterval`, 5 s. Wireless
|
||||
* draws its station list from the first poll and sat spinning for 4950 ms against ~360 ms on
|
||||
* a full load.
|
||||
*
|
||||
* stop() alone is NOT the fix: it deletes `tick`, and Poll.add() only auto-starts when
|
||||
* `tick != null`, so the incoming pollers would never start at all. stop()+start() on an
|
||||
* EMPTY queue leaves what a fresh document has (`tick = 0`, no timer armed); the view's first
|
||||
* poll.add() then starts it and steps immediately — upstream's own sequence, since on a full
|
||||
* load initDOM() runs Poll.start() on an empty queue before the view renders. */
|
||||
if (L.Poll && L.Poll.queue) {
|
||||
L.Poll.queue.length = 0;
|
||||
L.Poll.stop();
|
||||
L.Poll.start();
|
||||
}
|
||||
/* kill the outgoing view's plain setInterval pollers too (podkop's log tailer) — a full load
|
||||
* 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). 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();
|
||||
|
||||
/* Keep <body data-page> in sync with the route: the server stamps the dispatch path
|
||||
* (`ctx.path`) on every full load, and page-scoped CSS keys off it. `rsegs` is the RESOLVED
|
||||
* leaf, so a firstchild URL like /admin/status yields the same "admin-status-overview"
|
||||
* whether it arrives as a full load or a client nav. Without the re-stamp the incoming page
|
||||
* keeps the previous page's data-page and its scoped styles silently do not apply.
|
||||
*
|
||||
* WHERE THIS SITS, AND WHAT IT COSTS. It has to be before the staged render: page-scoped CSS
|
||||
* is keyed on `body[data-page]`, so a view that renders under the wrong value measures itself
|
||||
* through the wrong rules — and the fitters run inside the stage. It cannot be moved to the
|
||||
* swap for the same reason. What that buys is a correct incoming page; what it costs is that
|
||||
* the OUTGOING page, still on screen until the swap, wears the incoming page's name for the
|
||||
* staging window. The one artifact that is visible today is the Overview's own
|
||||
* `<h2 name="content">Status</h2>`, hidden by a `body[data-page='admin-status-overview']`
|
||||
* rule and therefore un-hidden while leaving that page — the same stray heading the sweep at
|
||||
* the swap removes. Moving the stamp later would trade that for a wrongly measured incoming
|
||||
* page, which is worse and lasts longer. Reported in review; stated here rather than left as
|
||||
* a claim the placement does not have. */
|
||||
document.body.setAttribute('data-page', rsegs.join('-'));
|
||||
|
||||
/* …and hand the new page to fs-sheets, which darkens every foreign sheet that belongs to a
|
||||
* DIFFERENT page and re-lights the ones that belong to this one. This is what lets an
|
||||
* invasive sheet stay in the document without spending it — the alternative was a full load
|
||||
* on the way out of any page carrying one, which stock LuCI's five realtime views all do (see
|
||||
* the page-ownership block in fs-sheets.js). It must run AFTER the stamp above and BEFORE the
|
||||
* view renders, so nothing paints through a sheet that no longer owns the page.
|
||||
*
|
||||
* IN TWO HALVES, because unlike `data-page` this one can be split. Enabling the incoming
|
||||
* page's sheets is what the staged render needs; DISABLING the outgoing page's is what would
|
||||
* strip an app's own stylesheet off the content the user is still reading — for the whole
|
||||
* staging window, which on a cold route is the 600-1800 ms this design exists to fill. So the
|
||||
* page being left is spared here and swept at the swap (see commitStage). Reported in
|
||||
* review. */
|
||||
sheets.scopeToCurrentPage(rsegs, leaving);
|
||||
|
||||
const stage = stageView(contentHost);
|
||||
const painted = renderedIn(stage.view);
|
||||
_seen.add(className);
|
||||
/* NAME THE OWNER for the length of this require, AND ONLY WHEN THE MODULE HAS YET TO BE
|
||||
* EVALUATED. On a first visit the require IS the render, so any <style> the module injects
|
||||
* belongs to THIS page — even if a newer navigation has stamped data-page by the time it
|
||||
* lands, which is exactly the case repairStaleRender() below exists for. Without it fs-sheets
|
||||
* credited such a sheet to the page that superseded this one and bound it there for the life
|
||||
* of the document (measured: luci-app-filemanager's `.cbi-button-save { display: none
|
||||
* !important }` disabled on its own page and live on System -> System, across return visits).
|
||||
*
|
||||
* `!cached` is what makes the hint survive long enough to be read, and its absence is why the
|
||||
* measured case stopped being covered. A CACHED require injects nothing — the module was
|
||||
* evaluated on the first visit and `require()` hands back the singleton — but it still ran
|
||||
* this line, and its `.finally` fires within a microtask, so the navigation that superseded a
|
||||
* cold require would set the hint to itself and clear it to null while that require was still
|
||||
* in flight. The sheet then landed with no hint at all, i.e. credited to the superseding page,
|
||||
* which is the exact bug. A require that cannot inject has no business naming an owner. */
|
||||
* belongs to THIS page. Without it fs-sheets credited such a sheet to whichever page was
|
||||
* stamped when it landed and bound it there for the life of the document (measured:
|
||||
* luci-app-filemanager's `.cbi-button-save { display: none !important }` disabled on its own
|
||||
* page and live on System -> System, across return visits). A CACHED require injects nothing
|
||||
* — the module was evaluated on the first visit and `require()` hands back the singleton — so
|
||||
* it has no business naming an owner. */
|
||||
if (!cached) sheets.attributeTo(rsegs, gen);
|
||||
return RT.require(className).finally(() => { if (!cached) sheets.attributeTo(null, gen); });
|
||||
}).then((view) => {
|
||||
if (view == null) return;
|
||||
|
||||
/* `uciWarm` is awaited before the construct, never after: a cached module resolves within a
|
||||
* microtask, well before the refill lands, and the view would read the cache we just emptied.
|
||||
* It never rejects — the refill catches its own failure — so it cannot cost a full reload. */
|
||||
return Promise.resolve(uciWarm)
|
||||
.then(() => RT.require(className))
|
||||
.finally(() => { if (!cached) sheets.attributeTo(null, gen); })
|
||||
.then((view) => {
|
||||
if (!(view instanceof RT.view))
|
||||
throw new TypeError('Loaded class ' + className + ' is not a view');
|
||||
if (gen !== _navGen) {
|
||||
/* A newer navigation superseded this one. On the CACHED path nothing has happened
|
||||
* yet — skipping the constructor below is the whole cancellation. On the FIRST-visit
|
||||
* path the require() has ALREADY rendered into the live page and registered its
|
||||
* pollers, with nothing to cancel: undo it. See repairStaleRender(). */
|
||||
if (!cached)
|
||||
repairStaleRender(className);
|
||||
return;
|
||||
}
|
||||
if (cached) {
|
||||
/* singleton: its __init__ already ran, re-run it. Arm the paint-time guard BEFORE the
|
||||
* construct (__init__ binds this.render as it builds its chain) and stamp the
|
||||
* generation right after it (the first load() is a microtask, so this lands first) —
|
||||
* this navigation can still be superseded while that render awaits its RPC. */
|
||||
const cls = view.constructor;
|
||||
armRenderGuard(cls);
|
||||
const inst = new cls();
|
||||
inst.__fsGen = gen;
|
||||
}
|
||||
/* require() does not hand back a class — it caches an INSTANCE, so requiring a class
|
||||
* not seen before CONSTRUCTS it, and a view's __init__ IS its render. On a first
|
||||
* visit the require has therefore already painted (into the stage); only on a REVISIT
|
||||
* is there a singleton whose __init__ has to be re-run. `cached` was read before the
|
||||
* waits above, since the require is what fills LuCI's cache. */
|
||||
if (cached) new view.constructor();
|
||||
return painted;
|
||||
})
|
||||
.then(() => {
|
||||
/* superseded while rendering: the chain painted into ITS OWN stage, so there is
|
||||
* nothing to repair — drop it and leave the live page to the newer navigation. */
|
||||
if (gen !== _navGen) { dropStage(stage); return; }
|
||||
commitStage(stage, contentHost);
|
||||
/* now, and only now, is there one height to read: the incoming page's */
|
||||
if (restoreTo) restoreScroll(restoreTo, gen);
|
||||
})
|
||||
.catch((e) => { dropStage(stage); throw e; });
|
||||
}).catch((e) => {
|
||||
/* the full reload is a correct fallback, but swallowing the reason made every SPA-router
|
||||
* regression look like "the page is just slow to load". Log, then fall back. */
|
||||
console.error('footstrap: SPA nav to ' + className + ' failed, falling back to a full load', e);
|
||||
if (gen === _navGen) window.location = pathname;
|
||||
}).then(() => {
|
||||
progressEnd();
|
||||
release();
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -960,10 +1206,39 @@ function prefetchFrom(ev) {
|
||||
* lasts until the pointer happens to cross some other link. */
|
||||
let _lastHovered = null;
|
||||
|
||||
/* ---- a document the router could not have rendered is not one it may navigate AWAY from ----
|
||||
*
|
||||
* A `call`, `cbi` or `function` node — and a `template` other than the one Status→Overview node we
|
||||
* serve — is a page this theme did not build and cannot rebuild. What it leaves behind is the
|
||||
* problem: such a page may carry inline scripts and timers of its own, set before this module was
|
||||
* even evaluated, so the interval hook at the top of this file never saw them and no teardown of
|
||||
* ours can retire them. Only the document's death does. So the first click away from one is a full
|
||||
* load, which is what a user got before the router existed.
|
||||
*
|
||||
* NARROWER THAN "did the current path resolve", deliberately. A path that resolves to nothing is
|
||||
* not evidence of a hostile document — a wildcard URL (`admin/network/wireless/radio0.network1`)
|
||||
* simply is not in the tree, since nodeForSegs() stops at the first unknown segment, and refusing to
|
||||
* wire there would turn the router off for the whole document on some of the most-used pages in
|
||||
* LuCI. Only a node we CAN see and CANNOT serve disables it.
|
||||
*
|
||||
* On the stands this is a no-op today and is written for the case it is not: of 243 menu nodes,
|
||||
* the 110 `call` and 8 `function` ones answer with JSON or a redirect and never render this theme
|
||||
* at all (a probe on those URLs finds no `L` in the document), and the single `template` node is
|
||||
* the Overview, which viewClassFor() maps to `view.status.index`. */
|
||||
function bootDocumentIsOurs() {
|
||||
const node = tree.currentNode();
|
||||
if (!node || !node.action)
|
||||
return true; /* unknown to the tree: a wildcard page, and today's behaviour is right */
|
||||
return tree.viewClassFor(node) != null;
|
||||
}
|
||||
|
||||
function wireRouter() {
|
||||
if (_wired) return;
|
||||
_wired = true;
|
||||
|
||||
if (!bootDocumentIsOurs())
|
||||
return;
|
||||
|
||||
document.addEventListener('click', (ev) => {
|
||||
if (ev.defaultPrevented || ev.button !== 0 ||
|
||||
ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)
|
||||
@@ -1029,11 +1304,18 @@ function wireRouter() {
|
||||
* adopt the entry we arrived on and look up what IT recorded when it was left */
|
||||
saveScroll();
|
||||
adoptEntry();
|
||||
const target = _scrollMem.get(_curId);
|
||||
if (!navigate(window.location.pathname, false))
|
||||
/* Handed to navigate() rather than started here, and that is not tidiness — it is a bug the
|
||||
* staged swap introduced and this is where it is fixed. restoreScroll() writes as soon as the
|
||||
* scroller is TALL ENOUGH for the saved offset, and while the incoming page renders off
|
||||
* screen the OUTGOING one is still on it, so the height that satisfies the test can be the
|
||||
* old page's. Measured in the top layout: parked at 386, restored at 386 while Processes was
|
||||
* still up, then the swap replaced it with a shorter page and the browser clamped to 197.
|
||||
* Started after the commit instead, there is only one height it can be reading. */
|
||||
_pendingRestore = _scrollMem.get(_curId) || null;
|
||||
if (!navigate(window.location.pathname, false)) {
|
||||
_pendingRestore = null;
|
||||
window.location.reload();
|
||||
else
|
||||
restoreScroll(target, _navGen);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1054,6 +1336,11 @@ document.addEventListener('poll-stop', () => {
|
||||
}
|
||||
});
|
||||
|
||||
/* At module eval, like the listener above and for the same reason: the session can die during the
|
||||
* FIRST view's own data calls, i.e. before anything has called wire(), and an interceptor that was
|
||||
* not registered by then never sees the answer that said so. */
|
||||
watchSession();
|
||||
|
||||
/* Pause LuCI's 1s poll loop while the tab is hidden: LuCI has no visibilitychange handler, so an
|
||||
* overview left open in a background tab hammers ubus 24/7 (notably the pricey iwinfo getAssocList)
|
||||
* on a low-power router. stop() only clearInterval()s (the queue survives); start() re-arms and runs
|
||||
@@ -1073,7 +1360,10 @@ function wireVisibility() {
|
||||
wasActive = L.Poll.active();
|
||||
if (wasActive) L.Poll.stop();
|
||||
}
|
||||
else if (wasActive) {
|
||||
/* …but never resume a poll the session can no longer answer: luci-base stopped it on
|
||||
* purpose when it put its "Session expired" modal up, and restarting it on tab-show
|
||||
* would spend a burst of failing calls behind a page the user cannot use anyway. */
|
||||
else if (wasActive && !_expired) {
|
||||
L.Poll.start();
|
||||
}
|
||||
} catch (e) { console.error('footstrap: the poll pause/resume threw', e); }
|
||||
|
||||
@@ -733,12 +733,24 @@ function setEnabled(el, on) {
|
||||
* OWN are touched: a clean sheet is harmless and an invasive one we could not attribute still
|
||||
* poisons the document, so it keeps the full-load path rather than being silently disabled on its
|
||||
* own page. */
|
||||
function scopeToCurrentPage(segs) {
|
||||
/* `keep` is the page still ON SCREEN, and it exists because a client navigation now has a window in
|
||||
* which two pages are real: the incoming one renders into a hidden stage while the outgoing one is
|
||||
* still being read (fs-router's staged swap). Scoping is needed BEFORE the staged render — a view
|
||||
* must not measure itself through a sheet that does not own its page — but doing both halves then
|
||||
* strips the outgoing page's own stylesheet off content the user is looking at, for the 600-1800 ms
|
||||
* the staging window exists to fill. So the router calls this twice: once with `keep` set to the
|
||||
* page it is leaving, which enables the incoming page's sheets and leaves the departing page's
|
||||
* alone, and once with nothing at the swap, which is the ordinary sweep. */
|
||||
function scopeToCurrentPage(segs, keep) {
|
||||
if (segs) _curKey = appKey(segs);
|
||||
const key = currentKey();
|
||||
const spared = (keep && keep.length) ? appKey(keep) : null;
|
||||
document.querySelectorAll(VIEW_SHEETS).forEach((el) => {
|
||||
if (!outlivesPage(el) || !_owner.has(el)) return;
|
||||
setEnabled(el, _owner.get(el) === key);
|
||||
const owner = _owner.get(el);
|
||||
/* the page on screen keeps what it owns until the swap takes it off screen */
|
||||
if (spared !== null && owner === spared && owner !== key) return;
|
||||
setEnabled(el, owner === key);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,17 @@ const FS_VERSION = '0.0.0-dev';
|
||||
* `git describe` ('0.9.4-12-gabc1234') and that must keep counting as a real version. */
|
||||
function isReal() { return ((/^\d+\.\d+/).test(FS_VERSION)) && !((/-dev$/).test(FS_VERSION)); }
|
||||
|
||||
/* Two exports, and the two that were dropped are worth a line each so they are not re-added out of
|
||||
* habit: `VERSION` (the bare string) and `isReal` had no caller anywhere in the theme or the
|
||||
* templates — `label()` is what every reader actually wants, and it is the one place that decides
|
||||
* how a dev build names itself. An export nobody imports is not free: it is a promise to keep the
|
||||
* shape stable. */
|
||||
return baseclass.extend({
|
||||
VERSION: FS_VERSION,
|
||||
/* the project's page: the link at the foot of the Appearance tab, and the one
|
||||
* nameThemeInFooter() writes into the footer. 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. */
|
||||
/* the project's page: the link at the foot of the Appearance tab, and its only reader since
|
||||
* the footer line was withdrawn. 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,
|
||||
/* what the Appearance tab's version row prints */
|
||||
label: () => (isReal() ? ('Footstrap v' + FS_VERSION) : 'Footstrap (dev)')
|
||||
});
|
||||
@@ -58,8 +58,6 @@ 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. */
|
||||
|
||||
@@ -312,8 +312,18 @@ return baseclass.extend({
|
||||
|
||||
/* a clamp computed at the old width is wrong at the new one; coalesced via fit.frame
|
||||
* (the shared coalescer) because resize fires dozens of times a second while a window
|
||||
* is dragged */
|
||||
window.addEventListener('resize', fit.frame(clearClamps));
|
||||
* is dragged.
|
||||
*
|
||||
* WIDTH ONLY, for the reason fs-fit's observer states at length: every browser on iOS
|
||||
* fires `resize` continuously while the URL bar slides away, and a clamp computed for a
|
||||
* width that did not change is work done on a page the user is scrolling. */
|
||||
let lastWidth = window.innerWidth;
|
||||
const reclamp = fit.frame(clearClamps);
|
||||
window.addEventListener('resize', () => {
|
||||
if (window.innerWidth === lastWidth) return;
|
||||
lastWidth = window.innerWidth;
|
||||
reclamp();
|
||||
});
|
||||
|
||||
/* Appearance -> Submenus -> auto-collapse. fs-prefs.js owns the stored value and says only
|
||||
* that it changed; applying it is entirely ours, because every piece of the state is —
|
||||
|
||||
@@ -35,7 +35,7 @@ msgctxt "footstrap"
|
||||
msgid "Auto-collapse"
|
||||
msgstr "Colapso automático"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:656
|
||||
msgctxt "footstrap"
|
||||
msgid "Background"
|
||||
msgstr "Fondo"
|
||||
@@ -76,7 +76,7 @@ msgid "Collapse menu"
|
||||
msgstr "Contraer menú"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:650
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
msgctxt "footstrap"
|
||||
msgid "Colours"
|
||||
msgstr "Colores"
|
||||
@@ -124,7 +124,7 @@ msgctxt "footstrap"
|
||||
msgid "Dark"
|
||||
msgstr "Oscuro"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:657
|
||||
msgctxt "footstrap"
|
||||
msgid "Defaults"
|
||||
msgstr "Predeterminados"
|
||||
@@ -180,7 +180,7 @@ msgctxt "footstrap"
|
||||
msgid "Hard to read %s — large text only"
|
||||
msgstr "Se lee mal %s: solo texto grande"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:649
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:652
|
||||
msgctxt "footstrap"
|
||||
msgid "Interface"
|
||||
msgstr "Interfaz"
|
||||
@@ -213,10 +213,6 @@ msgctxt "footstrap"
|
||||
msgid "Light"
|
||||
msgstr "Claro"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-router.js:651
|
||||
msgid "Loading view…"
|
||||
msgstr "Cargando vista…"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:142
|
||||
msgid "Log in"
|
||||
msgstr "Iniciar sesión"
|
||||
@@ -331,7 +327,7 @@ msgctxt "footstrap"
|
||||
msgid "Rounding"
|
||||
msgstr "Redondeo"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:569
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:572
|
||||
msgctxt "footstrap"
|
||||
msgid "Router default"
|
||||
msgstr "Configuración predeterminada del enrutador"
|
||||
@@ -402,7 +398,7 @@ msgctxt "footstrap"
|
||||
msgid "Submenus"
|
||||
msgstr "Submenús"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:651
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
msgctxt "footstrap"
|
||||
msgid "Surfaces"
|
||||
msgstr "Superficies"
|
||||
@@ -415,7 +411,7 @@ msgstr "Sistema"
|
||||
msgid "System running in recovery (initramfs) mode."
|
||||
msgstr "El sistema se está ejecutando en modo de recuperación (initramfs)."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-select.js:587
|
||||
#: htdocs/luci-static/resources/fs-select.js:607
|
||||
msgid "Table"
|
||||
msgstr "Tabla"
|
||||
|
||||
@@ -569,6 +565,9 @@ msgid ""
|
||||
"the uploaded file could not be removed either; it is still on the router."
|
||||
msgstr "tampoco se pudo eliminar el archivo subido; sigue en el router."
|
||||
|
||||
#~ msgid "Loading view…"
|
||||
#~ msgstr "Cargando vista…"
|
||||
|
||||
#~ msgid "Logout"
|
||||
#~ msgstr "Cerrar sesión"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ msgctxt "footstrap"
|
||||
msgid "Auto-collapse"
|
||||
msgstr "Сворачивать"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:656
|
||||
msgctxt "footstrap"
|
||||
msgid "Background"
|
||||
msgstr "Фон"
|
||||
@@ -72,7 +72,7 @@ msgid "Collapse menu"
|
||||
msgstr "Свернуть меню"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:650
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
msgctxt "footstrap"
|
||||
msgid "Colours"
|
||||
msgstr "Цвета"
|
||||
@@ -118,7 +118,7 @@ msgctxt "footstrap"
|
||||
msgid "Dark"
|
||||
msgstr "Тёмная"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:657
|
||||
msgctxt "footstrap"
|
||||
msgid "Defaults"
|
||||
msgstr "Умолчания"
|
||||
@@ -174,7 +174,7 @@ msgctxt "footstrap"
|
||||
msgid "Hard to read %s — large text only"
|
||||
msgstr "Плохо читается %s — только крупный текст"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:649
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:652
|
||||
msgctxt "footstrap"
|
||||
msgid "Interface"
|
||||
msgstr "Интерфейс"
|
||||
@@ -207,10 +207,6 @@ msgctxt "footstrap"
|
||||
msgid "Light"
|
||||
msgstr "Светлая"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-router.js:651
|
||||
msgid "Loading view…"
|
||||
msgstr "Загрузка представления…"
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:142
|
||||
msgid "Log in"
|
||||
msgstr "Войти"
|
||||
@@ -324,7 +320,7 @@ msgctxt "footstrap"
|
||||
msgid "Rounding"
|
||||
msgstr "Скругление"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:569
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:572
|
||||
msgctxt "footstrap"
|
||||
msgid "Router default"
|
||||
msgstr "По умолчанию для роутера"
|
||||
@@ -395,7 +391,7 @@ msgctxt "footstrap"
|
||||
msgid "Submenus"
|
||||
msgstr "Подменю"
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:651
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
msgctxt "footstrap"
|
||||
msgid "Surfaces"
|
||||
msgstr "Поверхности"
|
||||
@@ -408,7 +404,7 @@ msgstr "Система"
|
||||
msgid "System running in recovery (initramfs) mode."
|
||||
msgstr "Система работает в режиме восстановления (initramfs)."
|
||||
|
||||
#: htdocs/luci-static/resources/fs-select.js:587
|
||||
#: htdocs/luci-static/resources/fs-select.js:607
|
||||
msgid "Table"
|
||||
msgstr "Таблица"
|
||||
|
||||
@@ -562,6 +558,9 @@ msgid ""
|
||||
"the uploaded file could not be removed either; it is still on the router."
|
||||
msgstr "загруженный файл тоже не удалось удалить — он остался на роутере."
|
||||
|
||||
#~ msgid "Loading view…"
|
||||
#~ msgstr "Загрузка представления…"
|
||||
|
||||
#~ msgid "Logout"
|
||||
#~ msgstr "Выход"
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ msgctxt "footstrap"
|
||||
msgid "Auto-collapse"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:656
|
||||
msgctxt "footstrap"
|
||||
msgid "Background"
|
||||
msgstr ""
|
||||
@@ -66,7 +66,7 @@ msgid "Collapse menu"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:344
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:650
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:653
|
||||
msgctxt "footstrap"
|
||||
msgid "Colours"
|
||||
msgstr ""
|
||||
@@ -112,7 +112,7 @@ msgctxt "footstrap"
|
||||
msgid "Dark"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:657
|
||||
msgctxt "footstrap"
|
||||
msgid "Defaults"
|
||||
msgstr ""
|
||||
@@ -166,7 +166,7 @@ msgctxt "footstrap"
|
||||
msgid "Hard to read %s — large text only"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:649
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:652
|
||||
msgctxt "footstrap"
|
||||
msgid "Interface"
|
||||
msgstr ""
|
||||
@@ -199,10 +199,6 @@ msgctxt "footstrap"
|
||||
msgid "Light"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-router.js:651
|
||||
msgid "Loading view…"
|
||||
msgstr ""
|
||||
|
||||
#: ucode/template/themes/footstrap/sysauth.ut:142
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
@@ -314,7 +310,7 @@ msgctxt "footstrap"
|
||||
msgid "Rounding"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:569
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:572
|
||||
msgctxt "footstrap"
|
||||
msgid "Router default"
|
||||
msgstr ""
|
||||
@@ -385,7 +381,7 @@ msgctxt "footstrap"
|
||||
msgid "Submenus"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:651
|
||||
#: htdocs/luci-static/resources/fs-appearance.js:654
|
||||
msgctxt "footstrap"
|
||||
msgid "Surfaces"
|
||||
msgstr ""
|
||||
@@ -398,7 +394,7 @@ msgstr ""
|
||||
msgid "System running in recovery (initramfs) mode."
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/fs-select.js:587
|
||||
#: htdocs/luci-static/resources/fs-select.js:607
|
||||
msgid "Table"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -16,11 +16,22 @@
|
||||
# `{% … %}` code block, so a `{#` that survives in this tree is a comment BY DEFINITION; if one
|
||||
# ever sat inside a <script> string the template would already be broken. Verified before
|
||||
# writing this: 0 of them appear inside a code block, and every opener has exactly one closer.
|
||||
# * `/* … */` inside `{% … %}` — a ucode CODE comment, 6844 bytes more. NOT touched. Stripping it
|
||||
# needs a lexer that knows ucode strings, and 17 of the `/*` in this tree sit OUTSIDE any code
|
||||
# block (they are CSS/JS comments in inline <style>/<script>). That is exactly the shape that
|
||||
# made the old `sed 's/;}/}/g'` in build-css.sh eat a data-URI: a scanner that cannot see
|
||||
# strings. Not worth 6.8 KB.
|
||||
# * `/* … */` STANDING ON ITS OWN LINES — a code comment, wherever it sits: ucode inside
|
||||
# `{% … %}`, JavaScript inside an inline <script>, CSS inside an inline <style>. 18362 bytes
|
||||
# across this tree, 8 KB of them in the shipped package after gzip, which is 11% of it.
|
||||
#
|
||||
# This used to be left alone with the argument that stripping it "needs a lexer that knows
|
||||
# ucode strings" — and that argument is right about the general case and wrong about this one.
|
||||
# The rule here is not "remove /* … */"; it is "remove a comment that OWNS its lines": `/*` is
|
||||
# the first non-blank thing on its line and `*/` is the last non-blank thing on its (possibly
|
||||
# later) line. For that to eat live code, a string literal would have to span lines AND contain
|
||||
# a line that is nothing but a comment — measured across every .ut in this tree: 18362 of 18362
|
||||
# comment bytes are whole-line, zero are inline, and no multi-line template literal contains a
|
||||
# line-leading `/*`. Anything that does not fit the rule is LEFT IN PLACE and counted, so the
|
||||
# day one appears the build says so instead of quietly changing the meaning of a template.
|
||||
#
|
||||
# What this deliberately does NOT do is minify: no joining of lines, no touching of anything
|
||||
# that is not a comment from column one. The output still reads like the source, minus the prose.
|
||||
#
|
||||
# Whitespace control is EMULATED, not ignored: `{#- …` also eats the whitespace before the comment
|
||||
# and `… -#}` the whitespace after it, which is how ucode itself renders them. Nearly every .ut here
|
||||
@@ -73,6 +84,41 @@ while IFS= read -r f; do
|
||||
}
|
||||
' "$f" > "$CUR"
|
||||
mv "$CUR" "$f"
|
||||
|
||||
# …and the code comments that own their lines, line by line rather than over the slurped file:
|
||||
# a rule about what a LINE is cannot be expressed over a byte stream, and going line by line is
|
||||
# also what makes it impossible to walk into a string literal by accident (see the header).
|
||||
CUR="$f.tmp$$"
|
||||
awk -v LEFT=0 '
|
||||
function blank(t) { return t ~ /^[ \t\r]*$/ }
|
||||
{
|
||||
line = $0
|
||||
if (!incomment) {
|
||||
# a comment that owns this line: nothing but whitespace before /*
|
||||
if (line ~ /^[ \t]*\/\*/) {
|
||||
# …and if it closes on the same line, the rest of that line must be blank too
|
||||
if (line ~ /\*\//) {
|
||||
rest = line; sub(/^.*\*\//, "", rest)
|
||||
if (blank(rest)) next
|
||||
LEFT++; print line; next # code after the closer: leave it whole
|
||||
}
|
||||
incomment = 1; next
|
||||
}
|
||||
# an inline comment (code before it) is left alone and counted
|
||||
if (line ~ /\/\*/) LEFT++
|
||||
print line; next
|
||||
}
|
||||
# inside a block comment: it may only end a line, or we would drop live code
|
||||
if (line ~ /\*\//) {
|
||||
rest = line; sub(/^.*\*\//, "", rest)
|
||||
incomment = 0
|
||||
if (!blank(rest)) { LEFT++; print rest }
|
||||
}
|
||||
next
|
||||
}
|
||||
END { if (LEFT > 0) printf "strip-templates: %d comment(s) not on lines of their own, left in place\n", LEFT | "cat 1>&2" }
|
||||
' "$f" > "$CUR"
|
||||
mv "$CUR" "$f"
|
||||
CUR=""
|
||||
a=$(wc -c < "$f")
|
||||
before=$((before + b))
|
||||
|
||||
@@ -662,6 +662,59 @@
|
||||
.fs-indicators { flex-wrap: wrap; }
|
||||
.fs-content { padding: var(--fs-space-5) var(--fs-content-pad); flex: 1 1 auto; width: 100%; max-width: var(--fs-content-max); margin: 0 auto; }
|
||||
|
||||
/* ---------- the staging area a client navigation renders into ----------
|
||||
* fs-router.js renders the incoming page into a second `#view` inside this wrapper, first in
|
||||
* tree order, and moves its children into the live one when the render completes — so the page
|
||||
* on screen is never emptied while the next one loads.
|
||||
*
|
||||
* HIDDEN BUT LAID OUT, and the three declarations are not interchangeable. `visibility: hidden`
|
||||
* takes it off the screen while keeping the box: several views size themselves from the element
|
||||
* they render into (the realtime graphs read `#view.offsetWidth` inside `render()`), and
|
||||
* `display: none` would hand them a zero width they keep for the life of the instance. `height:
|
||||
* 0` is what stops the page under it from moving, and `overflow: clip` keeps the content that
|
||||
* overflows that zero height from painting anyway — `clip` rather than `hidden` because a
|
||||
* scroll container would be a new scrollport in the middle of the content column. */
|
||||
.fs-staging { visibility: hidden; height: 0; overflow: clip; }
|
||||
|
||||
/* ---------- and the hairline that says a slow navigation is under way ----------
|
||||
* Shown only once a navigation outlives 150 ms (fs-router.js), so a warm click never flashes it.
|
||||
*
|
||||
* FIXED, AT THE TOP OF THE VIEWPORT, AND A CHILD OF <body> — all three are load-bearing and the
|
||||
* first draft got two of them wrong. In the content column it is a flex item of `.fs-main`, and
|
||||
* a column flex container shrank its 2px to ZERO (measured: height 0 in the sidebar layout,
|
||||
* where `.fs-shell` is 100dvh); with `margin-bottom: -2px` the content painted over what was
|
||||
* left. And it cannot live inside `.fs-shell` either: that box carries `contain: paint`, which
|
||||
* makes a fixed descendant position itself against the shell and be clipped by it. At the top of
|
||||
* the viewport is also where a browser puts its own progress, so it is the least surprising
|
||||
* place for ours.
|
||||
*
|
||||
* `transform` and `opacity` only — both composited, so this cannot cost the render it is
|
||||
* reporting on. Reduced motion keeps the bar and drops its animation: the information is the
|
||||
* point, the movement is not. */
|
||||
#fs-nav-progress {
|
||||
position: fixed; inset-inline: 0; top: 0;
|
||||
height: 2px;
|
||||
background: var(--fs-accent);
|
||||
transform: scaleX(0); transform-origin: left center;
|
||||
opacity: 0; pointer-events: none;
|
||||
z-index: var(--fs-z-tooltip);
|
||||
}
|
||||
:root[dir="rtl"] #fs-nav-progress { transform-origin: right center; }
|
||||
#fs-nav-progress[data-state="active"] {
|
||||
opacity: 1;
|
||||
transform: scaleX(.8);
|
||||
transition: transform 2s cubic-bezier(.1, .7, .1, 1), opacity 120ms linear;
|
||||
}
|
||||
#fs-nav-progress[data-state="done"] {
|
||||
opacity: 0;
|
||||
transform: scaleX(1);
|
||||
transition: transform 120ms ease-out, opacity 300ms linear 100ms;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#fs-nav-progress[data-state="active"],
|
||||
#fs-nav-progress[data-state="done"] { transition: none; transform: scaleX(1); }
|
||||
}
|
||||
|
||||
/* ---------- footer ----------
|
||||
* Bare `footer`, not `footer.fs-footer`: the theme owns footer paint outright (absorbed
|
||||
* from base, which keeps only the flex skeleton), so a third-party view emitting its own
|
||||
|
||||
@@ -224,6 +224,43 @@
|
||||
background: var(--fs-panel2);
|
||||
}
|
||||
|
||||
/* ---- A MEASUREMENT MUST NOT THROW THE READER ----
|
||||
*
|
||||
* `fitTables()` decides a data table's tier by taking its marks OFF and asking the engine
|
||||
* (fs-fit rule 1: a stacked table is a pile of flex rows and always "fits", so it has to be
|
||||
* measured un-stacked). The reading forces layout, so for the duration of that pass the document
|
||||
* really is laid out with a table that is several screens taller than the one on screen.
|
||||
*
|
||||
* Scroll anchoring is what turns that into a visible bug. The browser keeps the node under the
|
||||
* reader still by adjusting the scroll offset — and it re-picks that node whenever the offset
|
||||
* changes for any other reason (css-scroll-anchoring: an anchor is invalidated by a scroll, by a
|
||||
* suppression trigger, or by its own removal). While the user is scrolling, that is every frame,
|
||||
* so the pick can land inside a table in the un-stacked state the fitter is about to undo; the
|
||||
* next frame the mark is back, the node is somewhere else entirely, and the engine faithfully
|
||||
* "corrects" the offset by the difference. Measured on Status → Overview at 390x844 with the poll
|
||||
* driving real re-renders while the page scrolled: 18 movements nobody asked for in 1500 frames,
|
||||
* the worst 1877px, always a pair — down a screenful and back on the next frame. That is the
|
||||
* page jumping up and down under the reader.
|
||||
*
|
||||
* So the tables the fitter re-stamps are declared ineligible as anchors. They are the only
|
||||
* elements in the document whose layout is deliberately falsified and restored inside one pass,
|
||||
* and every other node stays eligible — including the section headings around them, which is what
|
||||
* keeps the good half of anchoring working: a lease appearing or a station leaving still shifts
|
||||
* nothing, because the engine anchors on a heading and compensates for the height change. That
|
||||
* half was measured too, before and after: 0px of movement under the reader across six poll ticks
|
||||
* that grew and shrank a section above the viewport.
|
||||
*
|
||||
* NOT on the scroll container, which is the answer that suggests itself and is worse: with
|
||||
* `html, body, #maincontent { overflow-anchor: none }` the same run still moved 12 times, up to
|
||||
* 788px, because it also throws away the compensation the paragraph above depends on. Correct
|
||||
* anchoring, minus the one region that lies about its height.
|
||||
*
|
||||
* The cost is that a row of a data table can no longer be the anchor. On the page where that
|
||||
* would matter most — Processes at 390px, which is one long table — inserting a row above the
|
||||
* reader moves the content 285px with this rule and 285px without it: those rows were never being
|
||||
* chosen anyway. */
|
||||
.table.fs-dt { overflow-anchor: none; }
|
||||
|
||||
.cbi-section .table:is([id], .fs-dt) .tr:not(.table-titles):not(.placeholder):hover .td { background: var(--fs-panel2); }
|
||||
.cbi-section .table:is([id], .fs-dt) .tr.table-titles .th { border-bottom-color: var(--fs-border); }
|
||||
/* Contour around the whole data table. A real <table> needs border-collapse:separate or the
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mihomo-alpha
|
||||
PKG_VERSION:=2026.08.13
|
||||
PKG_RELEASE:=19
|
||||
PKG_VERSION:=2026.08.16
|
||||
PKG_RELEASE:=20
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||
PKG_SOURCE_VERSION:=3cac869656f8ce2793afced148ebd5d4280b3e08
|
||||
PKG_SOURCE_VERSION:=ac017cdd246ce8bd547653d927e7bf77d7ee73d5
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL3.0+
|
||||
@@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
PKG_BUILD_VERSION:=alpha-3cac869
|
||||
PKG_BUILD_VERSION:=alpha-ac017cd
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=mihomo-meta
|
||||
PKG_VERSION:=1.19.29
|
||||
PKG_RELEASE:=13
|
||||
PKG_VERSION:=1.19.30
|
||||
PKG_RELEASE:=14
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||
PKG_SOURCE_VERSION:=v1.19.29
|
||||
PKG_SOURCE_VERSION:=v1.19.30
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL3.0+
|
||||
@@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
PKG_BUILD_VERSION:=v1.19.29
|
||||
PKG_BUILD_VERSION:=v1.19.30
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=miniupnpd
|
||||
PKG_VERSION:=2.3.10
|
||||
PKG_RELEASE:=7
|
||||
PKG_VERSION:=2.3.11
|
||||
PKG_RELEASE:=8
|
||||
|
||||
PKG_SOURCE_URL:=https://github.com/miniupnp/miniupnp/releases/download/miniupnpd_$(subst .,_,$(PKG_VERSION))
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
|
||||
@@ -51,7 +51,7 @@ stop_service() {
|
||||
else
|
||||
nft flush chain inet fw4 upnp_forward 2>/dev/null
|
||||
nft flush chain inet fw4 upnp_prerouting 2>/dev/null
|
||||
#nft flush chain inet fw4 upnp_postrouting 2>/dev/null
|
||||
#nft flush chain inet fw4 upnp_postrouting 2>/dev/null # Not used with nftables
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ upnpd_generate_config() {
|
||||
echo "upnp_nat_table_name=fw4"
|
||||
echo "upnp_forward_chain=upnp_forward"
|
||||
echo "upnp_nat_chain=upnp_prerouting"
|
||||
#echo "upnp_nat_postrouting_chain=upnp_postrouting"
|
||||
#echo "upnp_nat_postrouting_chain=upnp_postrouting" # Not used with nftables
|
||||
fi
|
||||
|
||||
echo "# External network interface"
|
||||
@@ -197,7 +197,8 @@ upnpd_generate_config() {
|
||||
[ "$ipv6_disable" = "0" ] && echo "ipv6_disable=no" || echo "ipv6_disable=yes"
|
||||
local rejectport
|
||||
for rejectport in $reject_ports; do
|
||||
is_port_or_range "$rejectport" allowport0 && echo "deny $rejectport 0.0.0.0/0 $rejectport # Reject port $rejectport" ||
|
||||
is_port_or_range "$rejectport" allowport0 && echo "deny 1-65535 0.0.0.0/0 $rejectport # Reject port $rejectport" &&
|
||||
echo "deny $rejectport 0.0.0.0/0 1-65535 # Reject external port $rejectport" ||
|
||||
log "Invalid port or port range ($rejectport) in reject_ports ignored" daemon.warn
|
||||
done
|
||||
[ "$check_acl" = "1" ] && echo "# Access control list" && config_foreach upnpd_add_acl_entry acl_entry
|
||||
@@ -252,9 +253,9 @@ upnpd_add_acl_entry() {
|
||||
local cfg="$1"
|
||||
local comment int_addr int_port ext_port descr_filter action
|
||||
config_get comment "$cfg" comment "unspecified" # comment
|
||||
config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 or network and subnet mask (internal)
|
||||
config_get int_port "$cfg" int_port "1-65535" # internal port/range: x or x-y
|
||||
config_get ext_port "$cfg" ext_port "1-65535" # external port/range: x or x-y
|
||||
config_get int_addr "$cfg" int_addr "0.0.0.0/0" # IPv4 address or address and netmask (internal)
|
||||
config_get int_port "$cfg" int_port "1-65535" # internal port or range: x or x-y
|
||||
config_get ext_port "$cfg" ext_port "1-65535" # external port or range: x or x-y
|
||||
config_get descr_filter "$cfg" descr_filter # description regex filter (must be built in)
|
||||
config_get action "$cfg" action # accept/reject/disabled
|
||||
! is_port_or_range "$int_port" allowport0 &&
|
||||
|
||||
@@ -11,12 +11,7 @@ log() {
|
||||
log "Check UCI options in /etc/config/upnpd to be migrated to v2.0"
|
||||
cp /etc/config/upnpd /tmp
|
||||
|
||||
# Set missing enabled option to fix previously different defaults in LuCI/config (0) and init UCI (1)
|
||||
if ! uci -q get upnpd.config.enabled >/dev/null; then
|
||||
uci -q set upnpd.config.enabled="1"
|
||||
fi
|
||||
|
||||
# Migrate boolean options to only use 0/1 for LuCI support
|
||||
# Migrate boolean options to only use 0/1 for LuCI flag support
|
||||
for option in enabled ipv6_disable system_uptime; do
|
||||
if uci -q get upnpd.config.$option >/dev/null; then
|
||||
uci get upnpd.config.$option | grep -q -E -x "0|off|false|no|disabled" && uci set upnpd.config.$option="0"
|
||||
@@ -24,6 +19,11 @@ for option in enabled ipv6_disable system_uptime; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Set missing enabled option to fix previously different defaults in LuCI/config (0) and init UCI (1)
|
||||
if ! uci -q get upnpd.config.enabled >/dev/null; then
|
||||
uci -q set upnpd.config.enabled="1"
|
||||
fi
|
||||
|
||||
# Migrate enable_upnp/enable_natpmp -> enable_protocols: Combined option
|
||||
if uci -q get upnpd.config.enable_upnp >/dev/null || uci -q get upnpd.config.enable_natpmp >/dev/null; then
|
||||
log "enable_upnp/enable_natpmp -> enable_protocols: Combined option"
|
||||
@@ -47,6 +47,20 @@ if uci -q get upnpd.config.use_stun >/dev/null; then
|
||||
uci rename upnpd.config.use_stun="allow_cgnat"
|
||||
fi
|
||||
|
||||
# Remove known unsupported (not CGNAT filtering test capable) STUN servers and include stun_port in stun_host
|
||||
if uci -q get upnpd.config.stun_host | grep -q -E "stun[0-9]?.l.google.com|stun.cloudflare.com"; then
|
||||
log "stun_host: Unsupported STUN server ($(uci -q get upnpd.config.stun_host)) set, remove to set default"
|
||||
uci delete upnpd.config.stun_host
|
||||
uci -q delete upnpd.config.stun_port
|
||||
# To keep behaviour with daemon <2.3.10 as previously, a false-negative filter result was returned for unsupported servers
|
||||
uci set upnpd.config.allow_cgnat="allow-filtered"
|
||||
elif uci -q get upnpd.config.stun_port >/dev/null; then
|
||||
uci -q get upnpd.config.stun_host >/dev/null && [ "$(uci -q get upnpd.config.stun_port)" != "3478" ] &&
|
||||
log "stun_port: Include stun_port in stun_host, and remove option" &&
|
||||
uci set upnpd.config.stun_host="$(uci -q get upnpd.config.stun_host | cut -d ":" -f 1):$(uci -q get upnpd.config.stun_port)"
|
||||
uci delete upnpd.config.stun_port
|
||||
fi
|
||||
|
||||
# Migrate force_forwarding=1 (in X-Wrt since 2021) to new similar option allow_cgnat=allow-filtered for cross-upgrades
|
||||
if uci -q get upnpd.config.force_forwarding >/dev/null; then
|
||||
log "force_forwarding=1 -> allow_cgnat=allow-filtered: New option"
|
||||
@@ -55,23 +69,9 @@ if uci -q get upnpd.config.force_forwarding >/dev/null; then
|
||||
uci delete upnpd.config.force_forwarding
|
||||
fi
|
||||
|
||||
# Remove known unsupported (not CGNAT filtering test capable) STUN servers and include stun_port in stun_host
|
||||
if uci -q get upnpd.config.stun_host | grep -q -E "stun[0-9]?.l.google.com|stun.cloudflare.com"; then
|
||||
log "stun_host: Unsupported STUN server ($(uci -q get upnpd.config.stun_host)) set, remove to set default"
|
||||
uci delete upnpd.config.stun_host
|
||||
# Workaround for false negative filtered STUN CGNAT test results for unsupported servers before daemon 2.3.10
|
||||
uci set upnpd.config.allow_cgnat="allow-filtered"
|
||||
uci -q delete upnpd.config.stun_port
|
||||
elif uci -q get upnpd.config.stun_port >/dev/null; then
|
||||
uci -q get upnpd.config.stun_host >/dev/null && [ "$(uci -q get upnpd.config.stun_port)" != "3478" ] &&
|
||||
log "stun_port: Include stun_port in stun_host, and remove option" &&
|
||||
uci set upnpd.config.stun_host="$(uci -q get upnpd.config.stun_host | cut -d ":" -f 1):$(uci -q get upnpd.config.stun_port)"
|
||||
uci delete upnpd.config.stun_port
|
||||
fi
|
||||
|
||||
# Migrate secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd: Invert and extend to PCP
|
||||
# Migrate secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd/pcp/1: Invert/extend to PCP
|
||||
if uci -q get upnpd.config.secure_mode >/dev/null; then
|
||||
log "secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd: Invert and extend to PCP"
|
||||
log "secure_mode=1/0 -> allow_third_party_mapping=0/upnp-igd/pcp/1: Invert/extend to PCP"
|
||||
uci get upnpd.config.secure_mode | grep -q -E -x "0|off|false|no|disabled" &&
|
||||
uci set upnpd.config.allow_third_party_mapping="upnp-igd" ||
|
||||
uci set upnpd.config.allow_third_party_mapping="0"
|
||||
@@ -87,7 +87,7 @@ if uci -q get upnpd.config.log_output >/dev/null; then
|
||||
uci set upnpd.config.log_output="default"
|
||||
fi
|
||||
|
||||
# Rename upnp_lease_file -> lease_file: To original daemon name, and remove if UCI default set
|
||||
# Rename upnp_lease_file -> lease_file: To daemon option name, and remove if UCI default set
|
||||
if uci -q get upnpd.config.upnp_lease_file >/dev/null; then
|
||||
if [ "$(uci -q get upnpd.config.upnp_lease_file)" = "/var/run/miniupnpd.leases" ]; then
|
||||
log "upnp_lease_file -> lease_file: Remove option as UCI default set"
|
||||
@@ -115,7 +115,7 @@ if uci -q get upnpd.config.download >/dev/null; then
|
||||
download="$(uci -q get upnpd.config.download)"
|
||||
if [ "$download" != "1024" ] && [ "$download" -ge "1" ] 2>/dev/null; then
|
||||
log "download -> download_kbps: Convert to kbit/s"
|
||||
download_kbps="$((download * 8 * 1000 / 1024))"
|
||||
download_kbps="$((download * 8 * 1024 / 1000))"
|
||||
uci set upnpd.config.download_kbps="$download_kbps"
|
||||
fi
|
||||
uci delete upnpd.config.download
|
||||
@@ -124,7 +124,7 @@ if uci -q get upnpd.config.upload >/dev/null; then
|
||||
upload="$(uci -q get upnpd.config.upload)"
|
||||
if [ "$upload" != "512" ] && [ "$upload" -ge "1" ] 2>/dev/null; then
|
||||
log "upload -> upload_kbps: Convert to kbit/s"
|
||||
upload_kbps="$((upload * 8 * 1000 / 1024))"
|
||||
upload_kbps="$((upload * 8 * 1024 / 1000))"
|
||||
uci set upnpd.config.upload_kbps="$upload_kbps"
|
||||
fi
|
||||
uci delete upnpd.config.upload
|
||||
@@ -234,10 +234,8 @@ if [ "$addtemplateentries" = "1" ]; then
|
||||
fi
|
||||
|
||||
# Rename UCI section config -> settings (v2.0)
|
||||
if uci -q get upnpd.config >/dev/null; then
|
||||
log "Rename UCI section config -> settings (v2.0)" && uci rename upnpd.config="settings" ||
|
||||
log "Error renaming the UCI section" daemon.err
|
||||
fi
|
||||
log "Rename UCI section config -> settings (v2.0)"
|
||||
uci rename upnpd.config="settings" || log "Error renaming the UCI section" daemon.err
|
||||
|
||||
uci commit upnpd >/dev/null
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 38e01919d585b0fd663d2b3249963b336061cbd3 Mon Sep 17 00:00:00 2001
|
||||
From f94e5bf7a60a41911af8d7e23cdb4ebe166257e2 Mon Sep 17 00:00:00 2001
|
||||
From: Self-Hosting-Group
|
||||
<155233284+Self-Hosting-Group@users.noreply.github.com>
|
||||
Date: Wed, 25 Mar 2026 00:00:00 +0000
|
||||
Date: Fri, 14 Aug 2026 00:00:00 +0000
|
||||
Subject: [PATCH] miniupnpd: UPnP IGDv2 Microsoft/Apple compatibility
|
||||
|
||||
* Add workaround to list port maps with the Windows IGDv2-incompatible
|
||||
@@ -20,7 +20,7 @@ Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Ove
|
||||
|
||||
--- a/upnphttp.c
|
||||
+++ b/upnphttp.c
|
||||
@@ -304,9 +304,22 @@ ParseHttpHeaders(struct upnphttp * h)
|
||||
@@ -307,9 +307,22 @@ ParseHttpHeaders(struct upnphttp * h)
|
||||
}
|
||||
else if(strncasecmp(line, "user-agent:", 11) == 0)
|
||||
{
|
||||
@@ -48,7 +48,7 @@ Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Ove
|
||||
}
|
||||
--- a/upnpsoap.c
|
||||
+++ b/upnpsoap.c
|
||||
@@ -854,6 +854,14 @@ GetSpecificPortMappingEntry(struct upnph
|
||||
@@ -920,6 +920,14 @@ GetSpecificPortMappingEntry(struct upnph
|
||||
#ifdef ENABLE_PCP
|
||||
hide_pcp_nonce(desc);
|
||||
#endif
|
||||
@@ -63,7 +63,7 @@ Link: https://github.com/Self-Hosting-Group/wiki/wiki/Port-Mapping-Protocols-Ove
|
||||
bodylen = snprintf(body, sizeof(body), resp,
|
||||
action, ns/*SERVICE_TYPE_WANIPC*/,
|
||||
(unsigned int)iport, int_ip, desc, leaseduration,
|
||||
@@ -1124,6 +1132,14 @@ GetGenericPortMappingEntry(struct upnpht
|
||||
@@ -1208,6 +1216,14 @@ GetGenericPortMappingEntry(struct upnpht
|
||||
#ifdef ENABLE_PCP
|
||||
hide_pcp_nonce(desc);
|
||||
#endif
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
From 38e01919d585b0fd663d2b3249963b336061cbd3 Mon Sep 17 00:00:00 2001
|
||||
From a7cebfc522306295cc1745c637f64d0e7d659795 Mon Sep 17 00:00:00 2001
|
||||
From: Self-Hosting-Group
|
||||
<155233284+Self-Hosting-Group@users.noreply.github.com>
|
||||
Date: Tue, 12 May 2026 00:00:00 +0000
|
||||
Date: Fri, 14 Aug 2026 00:00:00 +0000
|
||||
Subject: [PATCH] miniupnpd: Improve logging
|
||||
|
||||
* Clearer logging of enabled protocols/ports, IPv6 mapping, and UPnP IGD
|
||||
compatibility mode… in start banner
|
||||
* Log warnings with an enabled allow third-party mapping option
|
||||
* Less verbose logging by changing the log level of many normally
|
||||
occurring or internal messages, or those not relevant to users, to
|
||||
debug or info
|
||||
* Comment out log-filling messages when internal interfaces lose link,
|
||||
and for e.g. `rule with label '%s' is not a IGD pinhole`, as normally
|
||||
occurring
|
||||
* Change the log level (less verbose) of many internal or normally
|
||||
occurring messages, and some error messages, without functional
|
||||
restrictions, to DEBUG, client requests/actions and their errors to
|
||||
INFO, and only daemon-wide messages to NOTICE/WARNING/ERR…
|
||||
* Comment out some log-filling messages when internal interfaces lose
|
||||
link, and for e.g. `rule with label '%s' is not a IGD pinhole`, as
|
||||
normally occurring
|
||||
|
||||
Fixes: https://redirect.github.com/openwrt/packages/issues/17601
|
||||
Fixes: https://redirect.github.com/openwrt/packages/issues/26483
|
||||
Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
---
|
||||
asyncsendto.c | 4 +--
|
||||
getifaddr.c | 4 +--
|
||||
minissdp.c | 24 ++++++-------
|
||||
miniupnpd.c | 20 +++++------
|
||||
natpmp.c | 16 ++++-----
|
||||
netfilter/iptcrdr.c | 10 +++---
|
||||
netfilter/iptpinhole.c | 4 +--
|
||||
asyncsendto.c | 4 ++--
|
||||
getifaddr.c | 4 ++--
|
||||
minissdp.c | 24 ++++++++++++------------
|
||||
miniupnpd.c | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
|
||||
natpmp.c | 16 ++++++++--------
|
||||
netfilter/iptcrdr.c | 10 +++++-----
|
||||
netfilter/iptpinhole.c | 4 ++--
|
||||
netfilter_nft/nftnlrdr.c | 2 +-
|
||||
netfilter_nft/nftnlrdr_misc.c | 8 ++---
|
||||
netfilter_nft/nftpinhole.c | 10 +++---
|
||||
pcpserver.c | 18 +++++-----
|
||||
pf/pfpinhole.c | 8 ++---
|
||||
upnphttp.c | 6 ++--
|
||||
upnppinhole.c | 48 ++++++++++++-------------
|
||||
upnpredirect.c | 20 +++++------
|
||||
upnpsoap.c | 28 +++++++--------
|
||||
16 files changed, 115 insertions(+), 115 deletions(-)
|
||||
netfilter_nft/nftnlrdr_misc.c | 8 ++++----
|
||||
netfilter_nft/nftpinhole.c | 10 +++++-----
|
||||
pcpserver.c | 18 +++++++++---------
|
||||
pf/obsdrdr.c | 6 +++---
|
||||
pf/pfpinhole.c | 12 ++++++------
|
||||
upnphttp.c | 6 +++---
|
||||
upnppinhole.c | 48 ++++++++++++++++++++++++------------------------
|
||||
upnpredirect.c | 20 ++++++++++----------
|
||||
upnpsoap.c | 30 +++++++++++++++---------------
|
||||
17 files changed, 171 insertions(+), 143 deletions(-)
|
||||
|
||||
--- a/asyncsendto.c
|
||||
+++ b/asyncsendto.c
|
||||
@@ -72,7 +74,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
--- a/minissdp.c
|
||||
+++ b/minissdp.c
|
||||
@@ -741,8 +741,8 @@ SendSSDPNotify(int s, const struct socka
|
||||
@@ -743,8 +743,8 @@ SendSSDPNotify(int s, const struct socka
|
||||
}
|
||||
n = sendto_or_schedule(s, bufr, l, 0, dest, dest_len);
|
||||
if(n < 0) {
|
||||
@@ -83,7 +85,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
} else if(n != l) {
|
||||
syslog(LOG_NOTICE, "sendto() sent %d out of %d bytes", n, l);
|
||||
}
|
||||
@@ -752,8 +752,8 @@ SendSSDPNotify(int s, const struct socka
|
||||
@@ -754,8 +754,8 @@ SendSSDPNotify(int s, const struct socka
|
||||
* discovery messages SHOULD NOT be sent more than three times. */
|
||||
n = sendto_schedule(s, bufr, l, 0, dest, dest_len, 250);
|
||||
if(n < 0) {
|
||||
@@ -94,7 +96,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1060,7 +1060,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
}
|
||||
if(lan_addr == NULL)
|
||||
{
|
||||
@@ -103,7 +105,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
sender_str, source_if);
|
||||
return;
|
||||
}
|
||||
@@ -1161,7 +1161,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1163,7 +1163,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
sender_str );*/
|
||||
if(st && (st_len > 0))
|
||||
{
|
||||
@@ -112,7 +114,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
sender_str, st_len, st);
|
||||
/* find in which sub network the client is */
|
||||
#ifdef ENABLE_IPV6
|
||||
@@ -1274,7 +1274,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1276,7 +1276,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
else
|
||||
snprintf(ver_str, sizeof(ver_str), "%d", known_service_types[i].version);
|
||||
#endif
|
||||
@@ -121,7 +123,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
#ifdef DELAY_MSEARCH_RESPONSE
|
||||
delay = random() / (1 + RAND_MAX / (1000 * mx_value));
|
||||
#ifdef DEBUG
|
||||
@@ -1303,7 +1303,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1305,7 +1305,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
#ifdef DELAY_MSEARCH_RESPONSE
|
||||
unsigned int delay_increment = (mx_value * 1000) / 15;
|
||||
#endif
|
||||
@@ -130,7 +132,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
for(i=0; known_service_types[i].s; i++)
|
||||
{
|
||||
#ifdef DELAY_MSEARCH_RESPONSE
|
||||
@@ -1361,7 +1361,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1363,7 +1363,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
#endif
|
||||
if(0 == memcmp(st, uuidvalue_igd, l))
|
||||
{
|
||||
@@ -139,7 +141,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
SendSSDPResponse(s, sender, st, st_len, "",
|
||||
announced_host, http_port,
|
||||
#ifdef ENABLE_HTTPS
|
||||
@@ -1371,7 +1371,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1373,7 +1373,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
}
|
||||
else if(0 == memcmp(st, uuidvalue_wan, l))
|
||||
{
|
||||
@@ -148,7 +150,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
SendSSDPResponse(s, sender, st, st_len, "",
|
||||
announced_host, http_port,
|
||||
#ifdef ENABLE_HTTPS
|
||||
@@ -1381,7 +1381,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
@@ -1383,7 +1383,7 @@ ProcessSSDPData(int s, const char *bufr,
|
||||
}
|
||||
else if(0 == memcmp(st, uuidvalue_wcd, l))
|
||||
{
|
||||
@@ -157,7 +159,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
SendSSDPResponse(s, sender, st, st_len, "",
|
||||
announced_host, http_port,
|
||||
#ifdef ENABLE_HTTPS
|
||||
@@ -1442,7 +1442,7 @@ SendSSDPbyebye(int s, const struct socka
|
||||
@@ -1444,7 +1444,7 @@ SendSSDPbyebye(int s, const struct socka
|
||||
n = sendto_or_schedule(s, bufr, l, 0, dest, destlen);
|
||||
if(n < 0)
|
||||
{
|
||||
@@ -168,7 +170,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
else if(n != l)
|
||||
--- a/miniupnpd.c
|
||||
+++ b/miniupnpd.c
|
||||
@@ -489,7 +489,7 @@ ProcessIncomingHTTP(int shttpl, const ch
|
||||
@@ -496,7 +496,7 @@ ProcessIncomingHTTP(int shttpl, const ch
|
||||
if(get_lan_for_peer((struct sockaddr *)&clientname) == NULL)
|
||||
{
|
||||
/* The peer is not a LAN ! */
|
||||
@@ -177,7 +179,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
"%s peer %s is not from a LAN, closing the connection",
|
||||
protocol, addr_str);
|
||||
close(shttp);
|
||||
@@ -859,7 +859,7 @@ set_startup_time(void)
|
||||
@@ -867,7 +867,7 @@ set_startup_time(void)
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -186,7 +188,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
fclose(f);
|
||||
startup_time -= uptime;
|
||||
@@ -2109,6 +2109,22 @@ init(int argc, char * * argv, struct run
|
||||
@@ -2228,6 +2228,22 @@ init(int argc, char * * argv, struct run
|
||||
pidfilename = NULL;
|
||||
#endif
|
||||
|
||||
@@ -209,7 +211,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
#ifdef USE_SYSTEMD
|
||||
if (systemd_flag) {
|
||||
int r = sd_notify(0,
|
||||
@@ -2121,7 +2137,7 @@ init(int argc, char * * argv, struct run
|
||||
@@ -2240,7 +2256,7 @@ init(int argc, char * * argv, struct run
|
||||
|
||||
#ifdef ENABLE_LEASEFILE
|
||||
/*remove(lease_file);*/
|
||||
@@ -218,8 +220,8 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
reload_from_lease_file();
|
||||
#ifdef ENABLE_UPNPPINHOLE
|
||||
reload_from_lease_file6();
|
||||
@@ -2409,21 +2425,9 @@ main(int argc, char * * argv)
|
||||
return 0;
|
||||
@@ -2431,21 +2447,9 @@ main(int argc, char * * argv)
|
||||
goto shutdown;
|
||||
}
|
||||
|
||||
- syslog(LOG_INFO, "version " MINIUPNPD_VERSION " starting%s%sext if %s BOOTID=%u",
|
||||
@@ -241,25 +243,25 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2467,7 +2471,7 @@ main(int argc, char * * argv)
|
||||
return 1;
|
||||
}
|
||||
v.port = listen_port;
|
||||
@@ -2494,7 +2498,7 @@ main(int argc, char * * argv)
|
||||
#ifdef ENABLE_NFQUEUE
|
||||
nfqueue_data.http_port = listen_port;
|
||||
#endif /* ENABLE_NFQUEUE */
|
||||
- syslog(LOG_NOTICE, "HTTP listening on port %d", v.port);
|
||||
+ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTP) traffic on port %d/TCP, SSDP 1900/UDP", v.port);
|
||||
#if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6)
|
||||
if(!GETFLAG(IPV6DISABLEDMASK))
|
||||
{
|
||||
@@ -2493,7 +2497,7 @@ main(int argc, char * * argv)
|
||||
return 1;
|
||||
}
|
||||
v.https_port = listen_port;
|
||||
@@ -2525,7 +2529,7 @@ main(int argc, char * * argv)
|
||||
#ifdef ENABLE_NFQUEUE
|
||||
nfqueue_data.https_port = listen_port;
|
||||
#endif /* ENABLE_NFQUEUE */
|
||||
- syslog(LOG_NOTICE, "HTTPS listening on port %d", v.https_port);
|
||||
+ syslog(LOG_NOTICE, "Listening for UPnP IGD (SOAP/HTTPS) traffic on port %d/TCP", v.https_port);
|
||||
#if defined(V6SOCKETS_ARE_V6ONLY) && defined(ENABLE_IPV6)
|
||||
shttpsl_v4 = OpenAndConfHTTPSocket(&listen_port, 0);
|
||||
if(shttpsl_v4 < 0)
|
||||
@@ -2507,11 +2511,11 @@ main(int argc, char * * argv)
|
||||
@@ -2540,11 +2544,11 @@ main(int argc, char * * argv)
|
||||
if(!GETFLAG(IPV6DISABLEDMASK)) {
|
||||
if(find_ipv6_addr(lan_addrs.lh_first ? lan_addrs.lh_first->ifname : NULL,
|
||||
ipv6_addr_for_http_with_brackets, sizeof(ipv6_addr_for_http_with_brackets)) > 0) {
|
||||
@@ -273,7 +275,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
SETFLAG(IPV6DISABLEDMASK);
|
||||
}
|
||||
}
|
||||
@@ -2563,7 +2567,7 @@ main(int argc, char * * argv)
|
||||
@@ -2601,7 +2605,7 @@ main(int argc, char * * argv)
|
||||
if(SendSSDPGoodbye(snotify, addr_count * 2) < 0)
|
||||
#endif
|
||||
{
|
||||
@@ -282,7 +284,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
}
|
||||
#endif /* UPNP_STRICT */
|
||||
@@ -2588,16 +2592,16 @@ main(int argc, char * * argv)
|
||||
@@ -2626,16 +2630,16 @@ main(int argc, char * * argv)
|
||||
if(OpenAndConfNATPMPSockets(snatpmp) < 0)
|
||||
#ifdef ENABLE_PCP
|
||||
{
|
||||
@@ -303,7 +305,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
NATPMP_PORT);
|
||||
}
|
||||
#endif
|
||||
@@ -2706,6 +2710,30 @@ main(int argc, char * * argv)
|
||||
@@ -2746,6 +2750,30 @@ main(int argc, char * * argv)
|
||||
}
|
||||
#endif /* HAS_LIBCAP_NG */
|
||||
|
||||
@@ -334,7 +336,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
#ifdef USE_SYSTEMD
|
||||
if (v.systemd_notify) {
|
||||
upnp_update_status();
|
||||
@@ -3016,7 +3044,7 @@ main(int argc, char * * argv)
|
||||
@@ -3056,7 +3084,7 @@ main(int argc, char * * argv)
|
||||
}
|
||||
i = try_sendto(&writeset);
|
||||
if(i < 0) {
|
||||
@@ -343,7 +345,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
#ifdef USE_MINIUPNPDCTL
|
||||
for(ectl = ctllisthead.lh_first; ectl;)
|
||||
@@ -3116,7 +3144,7 @@ main(int argc, char * * argv)
|
||||
@@ -3156,7 +3184,7 @@ main(int argc, char * * argv)
|
||||
if(lan_addr == NULL) {
|
||||
char sender_str[64];
|
||||
sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str));
|
||||
@@ -352,7 +354,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
sender_str);
|
||||
continue;
|
||||
}
|
||||
@@ -3139,7 +3167,7 @@ main(int argc, char * * argv)
|
||||
@@ -3179,7 +3207,7 @@ main(int argc, char * * argv)
|
||||
if(lan_addr == NULL) {
|
||||
char sender_str[64];
|
||||
sockaddr_to_string((struct sockaddr *)&senderaddr, sender_str, sizeof(sender_str));
|
||||
@@ -361,7 +363,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
sender_str);
|
||||
continue;
|
||||
}
|
||||
@@ -3173,7 +3201,7 @@ main(int argc, char * * argv)
|
||||
@@ -3213,7 +3241,7 @@ main(int argc, char * * argv)
|
||||
/* process SSDP packets */
|
||||
if(sudp >= 0 && FD_ISSET(sudp, &readset))
|
||||
{
|
||||
@@ -370,7 +372,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
#ifdef ENABLE_HTTPS
|
||||
ProcessSSDPRequest(sudp, (unsigned short)v.port, (unsigned short)v.https_port);
|
||||
#else
|
||||
@@ -3183,7 +3211,7 @@ main(int argc, char * * argv)
|
||||
@@ -3223,7 +3251,7 @@ main(int argc, char * * argv)
|
||||
#ifdef ENABLE_IPV6
|
||||
if(sudpv6 >= 0 && FD_ISSET(sudpv6, &readset))
|
||||
{
|
||||
@@ -379,7 +381,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
#ifdef ENABLE_HTTPS
|
||||
ProcessSSDPRequest(sudpv6, (unsigned short)v.port, (unsigned short)v.https_port);
|
||||
#else
|
||||
@@ -3285,7 +3313,7 @@ main(int argc, char * * argv)
|
||||
@@ -3325,7 +3353,7 @@ main(int argc, char * * argv)
|
||||
|
||||
shutdown:
|
||||
|
||||
@@ -388,7 +390,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
#ifdef USE_SYSTEMD
|
||||
if (v.systemd_notify) {
|
||||
sd_notify(0,
|
||||
@@ -3304,7 +3332,7 @@ shutdown:
|
||||
@@ -3344,7 +3372,7 @@ shutdown:
|
||||
if(SendSSDPGoodbye(snotify, addr_count * 2) < 0)
|
||||
#endif
|
||||
{
|
||||
@@ -473,7 +475,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
--- a/netfilter/iptcrdr.c
|
||||
+++ b/netfilter/iptcrdr.c
|
||||
@@ -711,7 +711,7 @@ delete_filter_rule(const char * ifname,
|
||||
@@ -715,7 +715,7 @@ delete_filter_rule(const char * ifname,
|
||||
continue;
|
||||
}
|
||||
index = i;
|
||||
@@ -482,7 +484,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule");
|
||||
h = NULL;
|
||||
break;
|
||||
@@ -804,7 +804,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
@@ -808,7 +808,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
#endif
|
||||
if(r == 0)
|
||||
{
|
||||
@@ -491,7 +493,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
/* Now delete both rules */
|
||||
/* first delete the nat rule */
|
||||
h = iptc_init("nat");
|
||||
@@ -848,7 +848,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
@@ -852,7 +852,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
if(iaddr != e->ip.dst.s_addr)
|
||||
continue;
|
||||
index = i;
|
||||
@@ -500,7 +502,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
r = delete_rule_and_commit(index, h, miniupnpd_forward_chain, "delete_filter_rule");
|
||||
h = NULL;
|
||||
break;
|
||||
@@ -906,7 +906,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
@@ -910,7 +910,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
}
|
||||
|
||||
index = i;
|
||||
@@ -509,7 +511,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
r2 = delete_rule_and_commit(index, h, miniupnpd_nat_postrouting_chain, "delete_peer_rule");
|
||||
h = NULL;
|
||||
break;
|
||||
@@ -958,7 +958,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
@@ -962,7 +962,7 @@ delete_redirect_and_filter_rules(unsigne
|
||||
if(iaddr != e->ip.src.s_addr)
|
||||
continue;
|
||||
index = i;
|
||||
@@ -550,7 +552,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
|
||||
--- a/netfilter_nft/nftnlrdr_misc.c
|
||||
+++ b/netfilter_nft/nftnlrdr_misc.c
|
||||
@@ -104,7 +104,7 @@ nft_mnl_connect(void)
|
||||
@@ -110,7 +110,7 @@ nft_mnl_connect(void)
|
||||
return -1;
|
||||
}
|
||||
mnl_portid = mnl_socket_get_portid(mnl_sock);
|
||||
@@ -559,7 +561,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -745,7 +745,7 @@ refresh_nft_cache(struct rule_list *head
|
||||
@@ -751,7 +751,7 @@ refresh_nft_cache(struct rule_list *head
|
||||
errno = 0;
|
||||
ret = mnl_cb_run(buf, n, mnl_seq, mnl_portid, table_cb, &data);
|
||||
if (ret <= -1 /*== MNL_CB_ERROR*/) {
|
||||
@@ -568,7 +570,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
"refresh_nft_cache", ret);
|
||||
return -1;
|
||||
}
|
||||
@@ -1279,7 +1279,7 @@ nft_send_rule(struct nftnl_rule * rule,
|
||||
@@ -1288,7 +1288,7 @@ nft_send_rule(struct nftnl_rule * rule,
|
||||
|
||||
result = send_batch(batch);
|
||||
if (result < 0) {
|
||||
@@ -577,7 +579,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
"nft_send_rule", rule, (int)cmd, (int)chain_type, result);
|
||||
}
|
||||
}
|
||||
@@ -1462,7 +1462,7 @@ send_batch(struct mnl_nlmsg_batch *batch
|
||||
@@ -1474,7 +1474,7 @@ send_batch(struct mnl_nlmsg_batch *batch
|
||||
errno = 0;
|
||||
ret = mnl_cb_run(buf, n, 0, mnl_portid, NULL, NULL);
|
||||
if (ret <= -1 /*== MNL_CB_ERROR*/) {
|
||||
@@ -635,7 +637,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
|
||||
--- a/pcpserver.c
|
||||
+++ b/pcpserver.c
|
||||
@@ -1086,7 +1086,7 @@ static void CreatePCPMap(pcp_info_t *pcp
|
||||
@@ -1094,7 +1094,7 @@ static void CreatePCPMap(pcp_info_t *pcp
|
||||
else
|
||||
r = CreatePCPMap_NAT(pcp_msg_info);
|
||||
pcp_msg_info->result_code = r;
|
||||
@@ -644,7 +646,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
"PCP MAP: %s mapping %s %hu->%s:%hu '%s'",
|
||||
r == PCP_SUCCESS ? "added" : "failed to add",
|
||||
proto_itoa(pcp_msg_info->protocol),
|
||||
@@ -1128,7 +1128,7 @@ static void DeletePCPMap(pcp_info_t *pcp
|
||||
@@ -1136,7 +1136,7 @@ static void DeletePCPMap(pcp_info_t *pcp
|
||||
if(0 != strcmp(desc, pcp_msg_info->desc)) {
|
||||
/* nonce does not match */
|
||||
pcp_msg_info->result_code = PCP_ERR_NOT_AUTHORIZED;
|
||||
@@ -653,7 +655,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
iport, proto_itoa(pcp_msg_info->protocol));
|
||||
return;
|
||||
} else {
|
||||
@@ -1146,14 +1146,14 @@ static void DeletePCPMap(pcp_info_t *pcp
|
||||
@@ -1154,14 +1154,14 @@ static void DeletePCPMap(pcp_info_t *pcp
|
||||
desc, sizeof(desc),
|
||||
NULL /* lifetime */);
|
||||
if (uid < 0) {
|
||||
@@ -670,7 +672,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
iport, proto_itoa(pcp_msg_info->protocol));
|
||||
return;
|
||||
} else {
|
||||
@@ -1168,7 +1168,7 @@ static void DeletePCPMap(pcp_info_t *pcp
|
||||
@@ -1176,7 +1176,7 @@ static void DeletePCPMap(pcp_info_t *pcp
|
||||
syslog(LOG_INFO, "PCP: %s port %hu mapping removed",
|
||||
proto==IPPROTO_TCP?"TCP":"UDP", (pcp_msg_info->is_fw ? iport : eport2));
|
||||
} else {
|
||||
@@ -679,7 +681,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
pcp_msg_info->mapped_str, iport, proto_itoa(proto));
|
||||
pcp_msg_info->result_code = PCP_ERR_NO_RESOURCES;
|
||||
}
|
||||
@@ -1391,7 +1391,7 @@ static int processPCPRequest(void * req,
|
||||
@@ -1399,7 +1399,7 @@ static int processPCPRequest(void * req,
|
||||
CreatePCPMap(pcp_msg_info);
|
||||
}
|
||||
} else {
|
||||
@@ -688,7 +690,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
return pcp_msg_info->result_code;
|
||||
}
|
||||
|
||||
@@ -1601,7 +1601,7 @@ int ProcessIncomingPCPPacket(int s, unsi
|
||||
@@ -1609,7 +1609,7 @@ int ProcessIncomingPCPPacket(int s, unsi
|
||||
if (!GETFLAG(PCP_ALLOWTHIRDPARTYMASK)) {
|
||||
lan_addr = get_lan_for_peer(senderaddr);
|
||||
if(lan_addr == NULL) {
|
||||
@@ -697,7 +699,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
addr_str);
|
||||
return 0;
|
||||
}
|
||||
@@ -1722,7 +1722,7 @@ void PCPSendUnsolicitedAnnounce(int * so
|
||||
@@ -1730,7 +1730,7 @@ void PCPSendUnsolicitedAnnounce(int * so
|
||||
}
|
||||
len = sendto_or_schedule(sockets[i], buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_in));
|
||||
if( len < 0 ) {
|
||||
@@ -706,7 +708,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
}
|
||||
#ifdef ENABLE_IPV6
|
||||
@@ -1733,7 +1733,7 @@ void PCPSendUnsolicitedAnnounce(int * so
|
||||
@@ -1741,7 +1741,7 @@ void PCPSendUnsolicitedAnnounce(int * so
|
||||
addr6.sin6_port = htons(5350);
|
||||
len = sendto_or_schedule(socket6, buff, PCP_MIN_LEN, 0, (struct sockaddr *)&addr6, sizeof(struct sockaddr_in6));
|
||||
if( len < 0 ) {
|
||||
@@ -715,6 +717,35 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
}
|
||||
}
|
||||
#endif /* ENABLE_IPV6 */
|
||||
--- a/pf/obsdrdr.c
|
||||
+++ b/pf/obsdrdr.c
|
||||
@@ -601,7 +601,7 @@ delete_nat_rule(const char * ifname, uns
|
||||
pr.nr = i;
|
||||
if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
|
||||
{
|
||||
- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
r = -1;
|
||||
}
|
||||
else
|
||||
@@ -1290,7 +1290,7 @@ priv_delete_redirect_rule_check_desc(con
|
||||
pr.nr = i;
|
||||
if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
|
||||
{
|
||||
- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
r = -1;
|
||||
break;
|
||||
}
|
||||
@@ -1375,7 +1375,7 @@ syslog(LOG_DEBUG, "%2d port=%hu proto=%d
|
||||
pr.nr = i;
|
||||
if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
|
||||
{
|
||||
- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
r = -1;
|
||||
}
|
||||
else
|
||||
--- a/pf/pfpinhole.c
|
||||
+++ b/pf/pfpinhole.c
|
||||
@@ -246,12 +246,12 @@ int find_pinhole(const char * ifname,
|
||||
@@ -732,6 +763,15 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
continue;
|
||||
}
|
||||
#endif /* USE_LIBPFCTL */
|
||||
@@ -360,7 +360,7 @@ int delete_pinhole(unsigned short uid)
|
||||
pr.action = PF_CHANGE_REMOVE;
|
||||
pr.nr = i;
|
||||
if(ioctl(dev, DIOCCHANGERULE, &pr) < 0) {
|
||||
- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
release_ticket(dev, tnum);
|
||||
return -1;
|
||||
}
|
||||
@@ -565,7 +565,7 @@ int clean_pinhole_list(unsigned int * ne
|
||||
return -1;
|
||||
}
|
||||
@@ -750,9 +790,18 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
continue;
|
||||
}
|
||||
#endif /* USE_LIBPFCTL */
|
||||
@@ -602,7 +602,7 @@ int clean_pinhole_list(unsigned int * ne
|
||||
pr.action = PF_CHANGE_REMOVE;
|
||||
pr.nr = i;
|
||||
if(ioctl(dev, DIOCCHANGERULE, &pr) < 0) {
|
||||
- syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
+ syslog(LOG_DEBUG, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
|
||||
release_ticket(dev, tnum);
|
||||
return -1;
|
||||
}
|
||||
--- a/upnphttp.c
|
||||
+++ b/upnphttp.c
|
||||
@@ -514,7 +514,7 @@ ProcessHTTPPOST_upnphttp(struct upnphttp
|
||||
@@ -517,7 +517,7 @@ ProcessHTTPPOST_upnphttp(struct upnphttp
|
||||
if(h->req_soapActionOff > 0)
|
||||
{
|
||||
/* we can process the request */
|
||||
@@ -761,7 +810,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
h->req_soapActionLen, h->req_buf + h->req_soapActionOff);
|
||||
ExecuteSoapAction(h,
|
||||
h->req_buf + h->req_soapActionOff,
|
||||
@@ -819,7 +819,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
|
||||
@@ -822,7 +822,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
|
||||
for(i = 0; i<15 && *p != '\r'; i++)
|
||||
HttpVer[i] = *(p++);
|
||||
HttpVer[i] = '\0';
|
||||
@@ -770,7 +819,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
h->clientaddr_str, HttpCommand, HttpUrl, HttpVer);
|
||||
ParseHttpHeaders(h);
|
||||
if(h->req_HostOff > 0 && h->req_HostLen > 0) {
|
||||
@@ -899,7 +899,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
|
||||
@@ -902,7 +902,7 @@ ProcessHttpQuery_upnphttp(struct upnphtt
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -988,7 +1037,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ int reload_from_lease_file(void)
|
||||
@@ -175,7 +175,7 @@ int reload_from_lease_file(void)
|
||||
if(!lease_file) return -1;
|
||||
fd = fopen( lease_file, "r");
|
||||
if (fd==NULL) {
|
||||
@@ -997,7 +1046,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
return -1;
|
||||
}
|
||||
if(unlink(lease_file) < 0) {
|
||||
@@ -184,33 +184,33 @@ int reload_from_lease_file(void)
|
||||
@@ -191,33 +191,33 @@ int reload_from_lease_file(void)
|
||||
proto = line;
|
||||
p = strchr(line, ':');
|
||||
if(!p) {
|
||||
@@ -1036,7 +1085,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
continue;
|
||||
}
|
||||
*(desc++) = '\0';
|
||||
@@ -231,7 +231,7 @@ int reload_from_lease_file(void)
|
||||
@@ -238,7 +238,7 @@ int reload_from_lease_file(void)
|
||||
timestamp += current_time; /* convert to our time */
|
||||
#else
|
||||
if(timestamp <= (unsigned int)current_unix_time) {
|
||||
@@ -1045,7 +1094,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
eport, iaddr, iport, proto);
|
||||
continue;
|
||||
} else {
|
||||
@@ -391,7 +391,7 @@ upnp_redirect(const char * rhost, unsign
|
||||
@@ -398,7 +398,7 @@ upnp_redirect(const char * rhost, unsign
|
||||
#endif /* CHECK_PORTINUSE */
|
||||
} else {
|
||||
timestamp = (leaseduration > 0) ? upnp_time() + leaseduration : 0;
|
||||
@@ -1054,7 +1103,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
eport, iaddr, iport, protocol, desc);
|
||||
return upnp_redirect_internal(rhost, eport, iaddr, iport, proto,
|
||||
desc, timestamp);
|
||||
@@ -641,7 +641,7 @@ get_upnp_rules_state_list(int max_rules_
|
||||
@@ -648,7 +648,7 @@ get_upnp_rules_state_list(int max_rules_
|
||||
{
|
||||
if(tmp->to_remove)
|
||||
{
|
||||
@@ -1065,7 +1114,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
*p = tmp->next;
|
||||
--- a/upnpsoap.c
|
||||
+++ b/upnpsoap.c
|
||||
@@ -491,7 +491,7 @@ AddPortMapping(struct upnphttp * h, cons
|
||||
@@ -545,7 +545,7 @@ AddPortMapping(struct upnphttp * h, cons
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1074,7 +1123,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
ClearNameValueList(&data);
|
||||
SoapError(h, 402, "Invalid Args");
|
||||
return;
|
||||
@@ -690,7 +690,7 @@ AddAnyPortMapping(struct upnphttp * h, c
|
||||
@@ -750,7 +750,7 @@ AddAnyPortMapping(struct upnphttp * h, c
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1083,7 +1132,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
ClearNameValueList(&data);
|
||||
SoapError(h, 402, "Invalid Args");
|
||||
return;
|
||||
@@ -847,7 +847,7 @@ GetSpecificPortMappingEntry(struct upnph
|
||||
@@ -913,7 +913,7 @@ GetSpecificPortMappingEntry(struct upnph
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1092,7 +1141,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
action,
|
||||
r_host ? r_host : "NULL", ext_port, protocol, int_ip,
|
||||
(unsigned int)iport, desc, leaseduration);
|
||||
@@ -931,7 +931,7 @@ DeletePortMapping(struct upnphttp * h, c
|
||||
@@ -1003,7 +1003,7 @@ DeletePortMapping(struct upnphttp * h, c
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1101,7 +1150,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
action, eport, protocol);
|
||||
|
||||
/* if in secure mode, check the IP
|
||||
@@ -1047,7 +1047,7 @@ DeletePortMappingRange(struct upnphttp *
|
||||
@@ -1125,7 +1125,7 @@ DeletePortMappingRange(struct upnphttp *
|
||||
for(i = 0; i < number; i++)
|
||||
{
|
||||
r = upnp_delete_redirection(port_list[i], protocol);
|
||||
@@ -1110,7 +1159,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
action, port_list[i], protocol, r < 0 ? "failed" : "ok");
|
||||
}
|
||||
free(port_list);
|
||||
@@ -1112,7 +1112,7 @@ GetGenericPortMappingEntry(struct upnpht
|
||||
@@ -1196,7 +1196,7 @@ GetGenericPortMappingEntry(struct upnpht
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1119,27 +1168,39 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
|
||||
rhost[0] = '\0';
|
||||
r = upnp_get_redirection_infos_by_index((int)index, &eport, protocol, &iport,
|
||||
@@ -1635,7 +1635,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
int r;
|
||||
@@ -1777,7 +1777,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
struct addrinfo hints, *ai, *p;
|
||||
int found = 0;
|
||||
|
||||
- syslog(LOG_INFO, "%s: InternalClient %s is not an IPv6, assume hostname and convert",
|
||||
+ syslog(LOG_DEBUG, "%s: InternalClient %s is not an IPv6, assume hostname and convert",
|
||||
"PinholeVerification", int_ip);
|
||||
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
@@ -1655,8 +1655,8 @@ PinholeVerification(struct upnphttp * h,
|
||||
@@ -1788,7 +1788,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
r = getaddrinfo(int_ip, NULL, &hints, &ai);
|
||||
if (r != 0)
|
||||
{
|
||||
- syslog(LOG_WARNING, "%s: Failed to convert hostname '%s' to IP address : %s",
|
||||
+ syslog(LOG_INFO, "%s: Failed to convert hostname '%s' to IP address : %s",
|
||||
"PinholeVerification", int_ip, gai_strerror(r));
|
||||
SoapError(h, 402, "Invalid Args");
|
||||
return -1;
|
||||
@@ -1803,11 +1803,11 @@ PinholeVerification(struct upnphttp * h,
|
||||
|
||||
if (inet_ntop(AF_INET6, &result_ip, int_ip_resolved, INET6_ADDRSTRLEN) == NULL)
|
||||
{
|
||||
result_ip = ((struct sockaddr_in6 *)p->ai_addr)->sin6_addr;
|
||||
if (inet_ntop(AF_INET6, &result_ip, int_ip, sizeof(struct in6_addr)) == NULL)
|
||||
- syslog(LOG_WARNING, "%s: inet_ntop(): %m", "PinholeVerification");
|
||||
- syslog(LOG_INFO, "%s: InternalClient resolved as %s",
|
||||
+ syslog(LOG_DEBUG, "%s: inet_ntop(): %m", "PinholeVerification");
|
||||
+ syslog(LOG_DEBUG, "%s: InternalClient resolved as %s",
|
||||
"PinholeVerification", int_ip);
|
||||
+ syslog(LOG_INFO, "%s: inet_ntop(): %m", "PinholeVerification");
|
||||
SoapError(h, 501, "Action Failed");
|
||||
return -1;
|
||||
}
|
||||
- syslog(LOG_INFO, "%s: InternalClient \"%s\" resolved as %s",
|
||||
+ syslog(LOG_DEBUG, "%s: InternalClient \"%s\" resolved as %s",
|
||||
"PinholeVerification", int_ip, int_ip_resolved);
|
||||
found = 1;
|
||||
}
|
||||
@@ -1672,7 +1672,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
@@ -1823,7 +1823,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
freeaddrinfo(ai);
|
||||
if (!found)
|
||||
{
|
||||
@@ -1148,16 +1209,16 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
"PinholeVerification", int_ip);
|
||||
SoapError(h, 402, "Invalid Args");
|
||||
return -1;
|
||||
@@ -1680,7 +1680,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
}
|
||||
else
|
||||
@@ -1841,7 +1841,7 @@ PinholeVerification(struct upnphttp * h,
|
||||
/* no need to copy to int_ip_resolved, but we still need to fill result_ip up */
|
||||
if (inet_pton(AF_INET6, int_ip, &result_ip) <= 0)
|
||||
{
|
||||
- syslog(LOG_WARNING, "%s: Failed to convert hostname '%s' to IP address : %s",
|
||||
+ syslog(LOG_INFO, "%s: Failed to convert hostname '%s' to IP address : %s",
|
||||
"PinholeVerification", int_ip, gai_strerror(r));
|
||||
SoapError(h, 402, "Invalid Args");
|
||||
- syslog(LOG_ERR, "inet_pton(%s)", int_ip);
|
||||
+ syslog(LOG_DEBUG, "inet_pton(%s)", int_ip);
|
||||
SoapError(h, 501, "Action Failed");
|
||||
return -1;
|
||||
@@ -1813,7 +1813,7 @@ AddPinhole(struct upnphttp * h, const ch
|
||||
}
|
||||
@@ -1974,7 +1974,7 @@ AddPinhole(struct upnphttp * h, const ch
|
||||
inet_ntop(AF_INET6,
|
||||
&(((struct sockaddr_in6 *)p->ai_addr)->sin6_addr),
|
||||
rem_ip, sizeof(rem_ip));
|
||||
@@ -1166,7 +1227,7 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
rem_host = rem_ip;
|
||||
break;
|
||||
}
|
||||
@@ -1822,7 +1822,7 @@ AddPinhole(struct upnphttp * h, const ch
|
||||
@@ -1983,7 +1983,7 @@ AddPinhole(struct upnphttp * h, const ch
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1175,12 +1236,12 @@ Link: https://github.com/Self-Hosting-Group/miniupnp/tree/improve-logging
|
||||
rem_host, gai_strerror(err));
|
||||
#if 0
|
||||
SoapError(h, 402, "Invalid Args");
|
||||
@@ -2481,7 +2481,7 @@ SoapError(struct upnphttp * h, int errCo
|
||||
@@ -2696,7 +2696,7 @@ SoapError(struct upnphttp * h, int errCo
|
||||
char body[2048];
|
||||
int bodylen;
|
||||
|
||||
- syslog(LOG_INFO, "Returning UPnPError %d: %s", errCode, errDesc);
|
||||
+ syslog(LOG_DEBUG, "Returning UPnPError %d: %s", errCode, errDesc);
|
||||
bodylen = snprintf(body, sizeof(body), resp, errCode, errDesc);
|
||||
BuildResp2_upnphttp(h, 500, "Internal Server Error", body, bodylen);
|
||||
SendRespAndClose_upnphttp(h);
|
||||
if(bodylen < 0) {
|
||||
syslog(LOG_ERR, "snprintf() returned %d", bodylen);
|
||||
+1
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=momo
|
||||
PKG_VERSION:=2026.06.03
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
|
||||
PKG_LICENSE:=GPL-3.0+
|
||||
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
|
||||
|
||||
@@ -218,16 +218,25 @@ start_service() {
|
||||
procd_close_instance
|
||||
# cron
|
||||
local reload_cron; reload_cron=0
|
||||
## clear cron
|
||||
if grep -q "#momo" "/etc/crontabs/root"; then
|
||||
log "App" "Clear cron."
|
||||
sed -i "/#momo/d" "/etc/crontabs/root" > /dev/null 2>&1
|
||||
reload_cron=1
|
||||
fi
|
||||
## add restart cron if needed
|
||||
if [ "$scheduled_restart" = 1 ] && [ -n "$scheduled_restart_cron" ]; then
|
||||
log "App" "Set scheduled restart."
|
||||
echo "$scheduled_restart_cron /etc/init.d/momo restart #momo scheduled restart" >> "/etc/crontabs/root"
|
||||
reload_cron=1
|
||||
fi
|
||||
## add log scheduled clear cron if needed
|
||||
if [ "$log_scheduled_clear" = 1 ] && [ -n "$log_scheduled_clear_cron" ]; then
|
||||
log "App" "Set log scheduled clear."
|
||||
echo "$log_scheduled_clear_cron /etc/init.d/momo clear_logs #momo log scheduled clear" >> "/etc/crontabs/root"
|
||||
reload_cron=1
|
||||
fi
|
||||
## reload cron if needed
|
||||
if [ "$reload_cron" = 1 ]; then
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
@@ -440,8 +449,10 @@ cleanup() {
|
||||
sysctl -q -w net.bridge.bridge-nf-call-ip6tables=1
|
||||
fi
|
||||
# delete cron
|
||||
if grep -q "#momo" "/etc/crontabs/root"; then
|
||||
sed -i "/#momo/d" "/etc/crontabs/root" > /dev/null 2>&1
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
}
|
||||
|
||||
clear_logs() {
|
||||
|
||||
+2
-2
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=natflow
|
||||
PKG_VERSION:=20260531
|
||||
PKG_RELEASE:=71
|
||||
PKG_RELEASE:=72
|
||||
|
||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=6472300c8d6b9082112a27a6a7547ea6057577be
|
||||
PKG_SOURCE_VERSION:=fb8848ba93fa22b264e6351865d7d76c846f656b
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=nikki
|
||||
PKG_VERSION:=2026.04.08
|
||||
PKG_RELEASE:=13
|
||||
PKG_RELEASE:=14
|
||||
|
||||
PKG_LICENSE:=GPL3.0+
|
||||
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
|
||||
|
||||
@@ -275,16 +275,25 @@ start_service() {
|
||||
procd_close_instance
|
||||
# cron
|
||||
local reload_cron; reload_cron=0
|
||||
## clear cron
|
||||
if grep -q "#nikki" "/etc/crontabs/root"; then
|
||||
log "App" "Clear cron."
|
||||
sed -i "/#nikki/d" "/etc/crontabs/root" > /dev/null 2>&1
|
||||
reload_cron=1
|
||||
fi
|
||||
## add restart cron if needed
|
||||
if [ "$scheduled_restart" = 1 ] && [ -n "$scheduled_restart_cron" ]; then
|
||||
log "App" "Set scheduled restart."
|
||||
echo "$scheduled_restart_cron /etc/init.d/nikki restart #nikki scheduled restart" >> "/etc/crontabs/root"
|
||||
reload_cron=1
|
||||
fi
|
||||
## add log scheduled clear cron if needed
|
||||
if [ "$log_scheduled_clear" = 1 ] && [ -n "$log_scheduled_clear_cron" ]; then
|
||||
log "App" "Set log scheduled clear."
|
||||
echo "$log_scheduled_clear_cron /etc/init.d/nikki clear_logs #nikki log scheduled clear" >> "/etc/crontabs/root"
|
||||
reload_cron=1
|
||||
fi
|
||||
## reload cron if needed
|
||||
if [ "$reload_cron" = 1 ]; then
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
@@ -497,8 +506,10 @@ cleanup() {
|
||||
sysctl -q -w net.bridge.bridge-nf-call-ip6tables=1
|
||||
fi
|
||||
# delete cron
|
||||
if grep -q "#nikki" "/etc/crontabs/root"; then
|
||||
sed -i "/#nikki/d" "/etc/crontabs/root" > /dev/null 2>&1
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
}
|
||||
|
||||
clear_logs() {
|
||||
|
||||
@@ -8,12 +8,12 @@ PKG_NAME:=alwaysonline
|
||||
PKG_UPSTREAM_VERSION:=1.2.1
|
||||
PKG_UPSTREAM_GITHASH:=206292ca68e4d6c81b215163a7bbd0cd2eb36860
|
||||
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH))
|
||||
PKG_RELEASE:=11
|
||||
PKG_RELEASE:=12
|
||||
UCI_VERSION:=0.2025.01.25
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/Jamesits/alwaysonline.git
|
||||
PKG_SOURCE_VERSION:=df3039364e41a68a11ecd462b5e3491403b6ca9e
|
||||
PKG_SOURCE_VERSION:=db4a6dede32e564c6d2aa4bf3486fd2ac1b76eb7
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_UPSTREAM_VERSION)
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=sing-box
|
||||
PKG_VERSION:=1.13.18
|
||||
PKG_RELEASE:=25
|
||||
PKG_VERSION:=1.13.19
|
||||
PKG_RELEASE:=26
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
+3
-3
@@ -8,12 +8,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=torrserver
|
||||
PKG_VERSION:=MatriX.142.2
|
||||
PKG_RELEASE:=15
|
||||
PKG_VERSION:=MatriX.142.4
|
||||
PKG_RELEASE:=16
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git
|
||||
PKG_SOURCE_VERSION:=5be23f5c816e1a44aee4e435b5b7a5369c707cd8
|
||||
PKG_SOURCE_VERSION:=9b611cf7022b60f4385392ce37c83793235fe5c6
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
|
||||
Reference in new issue
Block a user