🎄 Sync 2026-08-17 19:39:03

This commit is contained in:
github-actions[bot] committed 2026-08-17 19:39:03 +08:00
1 parent 762c5f8cd4
commit 522b0fc0b6
58 files changed
+1231 -680

No files matched your search

+2 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"]
}
}
}
+1 -8
View File
@@ -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)
+25 -2
View File
@@ -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"
+2 -2
View File
@@ -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;
+4 -4
View File
@@ -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 ---------- */
+31 -10
View File
@@ -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);
+5 -1
View File
@@ -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;
+12 -10
View File
@@ -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" },
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 —
+10 -11
View File
@@ -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"
+10 -11
View File
@@ -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 ""
+51 -5
View File
@@ -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
+4 -4
View File
@@ -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)
+4 -4
View File
@@ -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
View File
@@ -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
+7 -6
View File
@@ -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 &&
+27 -29
View File
@@ -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
+5 -5
View File
@@ -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
+173 -112
View File
@@ -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
View File
@@ -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>
+11
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+11
View File
@@ -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() {
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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>