diff --git a/clashoo/Makefile b/clashoo/Makefile
index af0aec2d..d84bb409 100644
--- a/clashoo/Makefile
+++ b/clashoo/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo
-PKG_SHORT_SHA:=fbb6742
-PKG_COMMIT_DATE:=2026.09.16
+PKG_SHORT_SHA:=5019cc0
+PKG_COMMIT_DATE:=2026.09.19
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
PKG_RELEASE:=1
@@ -11,8 +11,8 @@ PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
-PKG_SOURCE_VERSION:=fbb674227d5cf5a3796a1dd1451849fa1872884a
-PKG_HASH:=ecdd7fb1d5847884b350317e09c6d366fe6ea57e96f3ac0cd928df110f541ac8
+PKG_SOURCE_VERSION:=5019cc090ed7cafb76643f964a76b1e97aee2985
+PKG_HASH:=52f127d0125a3fb8e17dc7fe48c8f01e831fbc56cf4c86d7674a11c990e71848
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
PKG_LICENSE:=GPL3.0+
diff --git a/doc/lucky1.png b/doc/lucky1.png
new file mode 100644
index 00000000..a5b41b6e
Binary files /dev/null and b/doc/lucky1.png differ
diff --git a/doc/lucky2.png b/doc/lucky2.png
new file mode 100644
index 00000000..ba4c026f
Binary files /dev/null and b/doc/lucky2.png differ
diff --git a/doc/lucky3.png b/doc/lucky3.png
new file mode 100644
index 00000000..6fa7982f
Binary files /dev/null and b/doc/lucky3.png differ
diff --git a/doc/taskplan1.png b/doc/taskplan1.png
deleted file mode 100644
index 1f8db7be..00000000
Binary files a/doc/taskplan1.png and /dev/null differ
diff --git a/doc/taskplan2.png b/doc/taskplan2.png
deleted file mode 100644
index edb45d6d..00000000
Binary files a/doc/taskplan2.png and /dev/null differ
diff --git a/doc/taskplan3.png b/doc/taskplan3.png
deleted file mode 100644
index d4794702..00000000
Binary files a/doc/taskplan3.png and /dev/null differ
diff --git a/doc/view.png b/doc/view.png
deleted file mode 100644
index 169e112f..00000000
Binary files a/doc/view.png and /dev/null differ
diff --git a/doc/view2.png b/doc/view2.png
deleted file mode 100644
index 9058c632..00000000
Binary files a/doc/view2.png and /dev/null differ
diff --git a/luci-app-dockerman/luasrc/controller/dockerman.lua b/luci-app-dockerman/luasrc/controller/dockerman.lua
index 55d8ac52..d77d2731 100644
--- a/luci-app-dockerman/luasrc/controller/dockerman.lua
+++ b/luci-app-dockerman/luasrc/controller/dockerman.lua
@@ -175,6 +175,7 @@ end
function action_events()
local logs = ""
local query ={}
+ local json_parse = luci.jsonc.parse
local dk = docker.new()
-- Get events from last 24 hours
@@ -187,7 +188,32 @@ function action_events()
local date = "unknown"
if v and v.time then
date = os.date("%Y-%m-%d %H:%M:%S", v.time)
+ -- 安全检查:确保events对象和body存在
+ if not events then
+ logs = "[ERROR] Failed to connect to Docker daemon. Please ensure Docker is running.\n"
+ elseif not events.body then
+ local error_msg = events.message or events.body and events.body.message or tostring(events.body) or "Unknown error"
+ logs = string.format("[ERROR] Docker API error (code: %d): %s\n", events.code or 0, error_msg)
+ elseif events.code ~= 200 then
+ local error_msg = events.body.message or events.body or "Unknown error"
+ logs = string.format("[ERROR] Docker API error (code: %d): %s\n", events.code or 0, error_msg)
+ elseif type(events.body) ~= "table" and type(events.body) ~= "string" then
+ logs = "[INFO] No events received in the last 24 hours.\n"
+ else
+ -- 统一处理:如果是字符串,转换为table;如果是table,直接使用
+ local event_list = {}
+ if type(events.body) == "string" and events.body ~= "" then
+ -- 尝试解析JSON数组格式
+ local parsed = json_parse(events.body)
+ if type(parsed) == "table" then
+ event_list = parsed
+ else
+ -- 解析失败,当作单条事件处理
+ event_list = {events.body}
end
+ elseif type(events.body) == "table" then
+ event_list = events.body
+ end
local name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
local action = v.Action or "unknown"
@@ -202,10 +228,125 @@ function action_events()
elseif v.Type == "image" then
local id = v.Actor and v.Actor.ID or "unknown"
logs = logs .. string.format("[%s] %s %s Image: %s Image name: %s\n", date, v.Type, action, id, name)
+ -- 检查事件列表是否为空
+ if #event_list == 0 then
+ logs = "[INFO] No events received in the last 24 hours.\n"
+ else
+ -- 遍历事件列表
+ for _, item in ipairs(event_list) do
+ local v = item
+ -- 如果是字符串,尝试解析JSON
+ if type(v) == "string" and v ~= "" then
+ v = json_parse(v)
+ end
+
+ -- 确保v是有效的table
+ if type(v) ~= "table" then
+ logs = logs .. string.format("[PARSE ERROR] Invalid event data\n")
+ else
+ -- 解析时间戳:可读时间格式
+ local ts = tonumber(v.time) or 0
+ local readable_time = os.date("%Y-%m-%d %H:%M:%S", ts)
+
+ -- 获取事件类型和动作
+ local event_type = v.Type or v.type or "unknown"
+ local action = v.Action or v.action or "unknown"
+
+ -- 根据事件类型格式化输出:时间戳单独一行,其他信息分行列出
+ if event_type == "container" then
+ local container_id = v.Actor and v.Actor.ID or "unknown"
+ -- 预计算显示值,避免在string.format参数中使用连接操作符
+ local short_id = #container_id > 12 and container_id:sub(1, 12) or container_id
+ local container_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
+ local image = v.Actor and v.Actor.Attributes and v.Actor.Attributes.image or ""
+ local display_name = container_name .. " (" .. short_id .. ")"
+
+ logs = logs .. string.format(
+ "%s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n\n",
+ readable_time,
+ "", "[TYPE]", event_type,
+ "", "[ACTION]", action,
+ "", "[CONTAINER]", display_name,
+ "", "[IMAGE]", image
+ )
+
+ elseif event_type == "network" then
+ local network_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
+ local container_id = v.Actor and v.Actor.Attributes and v.Actor.Attributes.container or ""
+ -- 预计算显示值
+ local short_container = #container_id > 12 and container_id:sub(1, 12) or container_id
+
+ logs = logs .. string.format(
+ "%s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n\n",
+ readable_time,
+ "", "[TYPE]", event_type,
+ "", "[ACTION]", action,
+ "", "[NETWORK]", network_name,
+ "", "[CONTAINER]", short_container
+ )
+
+ elseif event_type == "image" then
+ local image_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
+
+ logs = logs .. string.format(
+ "%s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n\n",
+ readable_time,
+ "", "[TYPE]", event_type,
+ "", "[ACTION]", action,
+ "", "[IMAGE]", image_name
+ )
+
+ elseif event_type == "volume" then
+ local volume_name = v.Actor and v.Actor.Attributes and v.Actor.Attributes.name or "unknown"
+
+ logs = logs .. string.format(
+ "%s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n\n",
+ readable_time,
+ "", "[TYPE]", event_type,
+ "", "[ACTION]", action,
+ "", "[VOLUME]", volume_name
+ )
+
+ else
+ -- 未知类型
+ local actor_id = v.Actor and v.Actor.ID or "unknown"
+ local short_id = #actor_id > 12 and actor_id:sub(1, 12) or actor_id
+
+ logs = logs .. string.format(
+ "%s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n" ..
+ "%-24s %-10s %-15s\n\n",
+ readable_time,
+ "", "[TYPE]", event_type,
+ "", "[ACTION]", action,
+ "", "[ID]", short_id
+ )
+ end
+ end
end
end
end
+ -- 如果最终logs仍为空
+ if logs == "" then
+ logs = "[INFO] No events received in the last 24 hours.\n"
+ end
+
luci.template.render("dockerman/logs", {self={syslog = logs, title="Events"}})
end
@@ -615,4 +756,4 @@ function untag_image(tag)
luci.http.prepare_content("application/json")
luci.http.write_json({message = msg or "unknow"})
end
-end
\ No newline at end of file
+end
diff --git a/luci-app-dockerman/luasrc/view/dockerman/logs.htm b/luci-app-dockerman/luasrc/view/dockerman/logs.htm
index 6cd2cb09..3897c742 100644
--- a/luci-app-dockerman/luasrc/view/dockerman/logs.htm
+++ b/luci-app-dockerman/luasrc/view/dockerman/logs.htm
@@ -4,9 +4,50 @@
<%:Events%>
<% end %>
+
+<%
+-- 安全处理:确保syslog存在且为字符串
+local log_content = self.syslog or ""
+local row_count = 2
+
+-- 安全检查:只有当log_content非空时才计算行数
+if log_content and log_content ~= "" then
+ local newline_count = 0
+ for _ in string.gmatch(log_content, "\n") do
+ newline_count = newline_count + 1
+ end
+ row_count = newline_count + 2
+ if row_count < 5 then
+ row_count = 5
+ end
+ if row_count > 50 then
+ row_count = 50
+ end
+end
+%>
+
-
+<% if log_content == "" then %>
+
+
<%:No events found in the last 24 hours.%>
+<% elseif string.find(log_content, "^%[ERROR%]") then %>
+
+
+ <%:Docker Error:%> <%=log_content:pcdata()%>
+
+<% elseif string.find(log_content, "^%[DEBUG%]") then %>
+
+
<%:Debug Info - Raw Response:%>
+
+<% elseif string.find(log_content, "^%[INFO%]") then %>
+
+
<%:No events found in the last 24 hours.%>
+<% else %>
+
+
+<% end %>
+
<% if self.title == "Events" then %>
<%+footer%>
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua
index fe6fd30e..1a95a0ad 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua
@@ -96,6 +96,7 @@ if load_urltest_options then -- [[ URLTest Start ]]
o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
+ o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -106,30 +107,6 @@ if load_urltest_options then -- [[ URLTest Start ]]
end
end
end
- -- 读取旧 DynamicList
- function o.cfgvalue(self, section)
- return table.concat(m:get(section, "urltest_node") or {}, " ")
- end
- -- 写入保持 DynamicList
- function o.write(self, section, value)
- local old = m:get(section, "urltest_node") or {}
- local new, set = {}, {}
- for v in value:gmatch("%S+") do
- new[#new + 1] = v
- set[v] = 1
- end
- for _, v in ipairs(old) do
- if not set[v] then
- m:set(section, "urltest_node", new)
- return
- end
- set[v] = nil
- end
- for _ in pairs(set) do
- m:set(section, "urltest_node", new)
- return
- end
- end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua
index c4a564a8..24ac84e9 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/2_xray.lua
@@ -90,6 +90,7 @@ if load_balancing_options then -- [[ Load balancing Start ]]
o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
+ o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -100,30 +101,6 @@ if load_balancing_options then -- [[ Load balancing Start ]]
end
end
end
- -- 读取旧 DynamicList
- function o.cfgvalue(self, section)
- return table.concat(m:get(section, "balancing_node") or {}, " ")
- end
- -- 写入保持 DynamicList
- function o.write(self, section, value)
- local old = m:get(section, "balancing_node") or {}
- local new, set = {}, {}
- for v in value:gmatch("%S+") do
- new[#new + 1] = v
- set[v] = 1
- end
- for _, v in ipairs(old) do
- if not set[v] then
- m:set(section, "balancing_node", new)
- return
- end
- set[v] = nil
- end
- for _ in pairs(set) do
- m:set(section, "balancing_node", new)
- return
- end
- end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua
index 91acbf79..c8178231 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/0_socks.lua
@@ -36,7 +36,7 @@ o = s:option(Flag, "firewall_allow", translate("Firewall Allow"))
o.default = "0"
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
-o.rmempty = false
+o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
index 65e69f99..eba1dbc9 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/1_sing-box.lua
@@ -50,7 +50,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
-o.cfgvalue = function(self, section, value)
+o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -79,7 +79,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
-o = s:option(DynamicList, "users", translate("User"))
+o = s:option(MultiValue, "users", translate("User"))
+o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -535,7 +536,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
-o.rmempty = false
+o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
index b126f021..9f7e7d92 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/2_xray.lua
@@ -46,7 +46,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
-o.cfgvalue = function(self, section, value)
+o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -72,7 +72,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
-o = s:option(DynamicList, "users", translate("User"))
+o = s:option(MultiValue, "users", translate("User"))
+o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -521,7 +522,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
-o.rmempty = false
+o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua
index 9c7f4cc2..ff5da791 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/3_ss-rust.lua
@@ -41,7 +41,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
-o.cfgvalue = function(self, section, value)
+o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -79,7 +79,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
-o.rmempty = false
+o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua
index 9453bdf4..898dedd9 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/4_ssr.lua
@@ -54,7 +54,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
-o.cfgvalue = function(self, section, value)
+o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -110,7 +110,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
-o.rmempty = false
+o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua
index b526498b..dadc54f1 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/5_hysteria2.lua
@@ -31,7 +31,7 @@ o.validate = function(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
-o.cfgvalue = function(self, section, value)
+o.cfgvalue = function(self, section)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
@@ -45,7 +45,8 @@ o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
-o = s:option(DynamicList, "users", translate("User"))
+o = s:option(MultiValue, "users", translate("User"))
+o.cast = "table"
for i, v in ipairs(user_list) do
o:value(v[".name"], v.username)
end
@@ -168,7 +169,7 @@ o.default = "0"
o:depends({ custom = false })
o = s:option(Value, "firewall_allow_src", translate("Source zone"))
-o.rmempty = false
+o.rmempty = not m.is_js_luci
o.nocreate = true
o.allowany = true
o.default = "wan"
diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua
index 93173da5..ecb70785 100644
--- a/luci-app-passwall/luasrc/passwall/api.lua
+++ b/luci-app-passwall/luasrc/passwall/api.lua
@@ -1542,11 +1542,18 @@ function set_type_cbi(s)
obj.option_prefix = s_self.option_prefix
obj.option = s_self.option_prefix .. option
obj.cfgvalue = function(self, section)
+ local v
if self.rewrite_option then
- return self.map:get(section, self.rewrite_option)
+ v = self.map:get(section, self.rewrite_option)
else
- return self.map:get(section, self.config_option)
+ v = self.map:get(section, self.config_option)
end
+ if util.instanceof(self, cbi.MultiValue) then
+ if v and self.cast == "table" then
+ return table.concat(v, " ")
+ end
+ end
+ return v
end
obj.write = function(self, section, value)
if s1.fields["type"]:formvalue(s_self.section) == s_self.type_name then
@@ -1564,6 +1571,21 @@ function set_type_cbi(s)
new_val = new_t
end
end
+ if util.instanceof(self, cbi.MultiValue) then
+ local new_t = {}
+ if type(value) == "table" then
+ new_t = table_remove_duplicates(value)
+ else
+ string.gsub(value, '[^' .. " " .. ']+', function(v)
+ new_t[#new_t + 1] = v
+ end)
+ end
+ if self.cast == "string" then
+ new_val = table.concat(new_t, " ")
+ else
+ new_val = new_t
+ end
+ end
if self.rewrite_option then
self.map:set(section, self.rewrite_option, new_val)
else
@@ -1619,6 +1641,7 @@ end
function type_cbi_section(s, s2)
for i, v in ipairs(s2.children) do
local o = s2.children[i]
+ o.section = s
s:append(o)
s.fields[o.option] = o
end
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua
index b1c3bd14..444b52b0 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua
@@ -99,6 +99,7 @@ if load_urltest_options then -- [[ URLTest Start ]]
o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
+ o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -109,28 +110,6 @@ if load_urltest_options then -- [[ URLTest Start ]]
end
end
end
- function o.cfgvalue(self, section)
- return table.concat(m:get(section, "urltest_node") or {}, " ")
- end
- function o.write(self, section, value)
- local old = m:get(section, "urltest_node") or {}
- local new, set = {}, {}
- for v in value:gmatch("%S+") do
- new[#new + 1] = v
- set[v] = 1
- end
- for _, v in ipairs(old) do
- if not set[v] then
- m:set(section, "urltest_node", new)
- return
- end
- set[v] = nil
- end
- for _ in pairs(set) do
- m:set(section, "urltest_node", new)
- return
- end
- end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua
index 0af6b4fc..9091b6a9 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua
@@ -91,6 +91,7 @@ if load_balancing_options then -- [[ Load balancing Start ]]
o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document"))
o:depends({ node_add_mode = "manual" })
o.widget = "checkbox"
+ o.cast = "table"
o.template = m:template_path("/cbi/nodes_multivalue")
o.group = {}
for k1, v1 in pairs(node_list) do
@@ -101,28 +102,6 @@ if load_balancing_options then -- [[ Load balancing Start ]]
end
end
end
- function o.cfgvalue(self, section)
- return table.concat(m:get(section, "balancing_node") or {}, " ")
- end
- function o.write(self, section, value)
- local old = m:get(section, "balancing_node") or {}
- local new, set = {}, {}
- for v in value:gmatch("%S+") do
- new[#new + 1] = v
- set[v] = 1
- end
- for _, v in ipairs(old) do
- if not set[v] then
- m:set(section, "balancing_node", new)
- return
- end
- set[v] = nil
- end
- for _ in pairs(set) do
- m:set(section, "balancing_node", new)
- return
- end
- end
o = s:option(MultiValue, "node_group", translate("Select Group"))
o:depends({ node_add_mode = "batch" })
diff --git a/luci-theme-shadcn/.dev/docs/patches.md b/luci-theme-shadcn/.dev/docs/patches.md
index 2d43fb35..0509454e 100644
--- a/luci-theme-shadcn/.dev/docs/patches.md
+++ b/luci-theme-shadcn/.dev/docs/patches.md
@@ -10,7 +10,7 @@ Write narrow `[data-page]`/class-scoped overrides with native declarations, stil
The `@reference` + `@apply` route still _compiles_ for theme-repo patches and has real upsides — build-time validation (a typo'd utility fails the build; a typo'd `var()` fails silently at runtime) and the shared `dark:`/`md:`/`hover:` vocabulary — native is the default for the size numbers above, not a hard gate. App-shipped patches bypass the build entirely and have always been plain-CSS-only.
-Globally-applicable chrome tweaks (e.g. icon opacity) belong in `_shared.css`, not here.
+Before writing a patch, ask whether the symptom is really page-specific. Something that breaks the same way on any app's page is a theme bug: fix it in the core CSS so every page gets the fix, and keep globally-applicable chrome tweaks (e.g. icon opacity) in `_shared.css`, not here. A patch is for what is genuinely local to one app — or, for a preference the theme should not adopt for everyone, the global `admin.css` case below.
## Discovery and matching
@@ -20,6 +20,8 @@ Globally-applicable chrome tweaks (e.g. icon opacity) belong in `_shared.css`, n
Because discovery is at render time, **any package — not just the theme — may drop a `.css` into `luci-static/shadcn/patches/`** and it takes effect immediately, no theme rebuild required.
+**`patches/admin.css` is therefore the global patch.** Every admin page's path starts with `admin`, so the shortest prefix matches all of them; sorting loads it first and the page-specific patches cascade on top (the login page loads no patches at all). That is how a user carries a preference the theme should not ship for everyone — a CJK-friendly monospace UI font, say — without forking it: write the file by hand on the device at `/www/luci-static/shadcn/patches/admin.css`. The theme neither builds nor installs that path, so a theme upgrade leaves it alone.
+
`PATCH_ALIASES` in `vite.config.ts` can duplicate one built payload (CSS and JS) under several page names when unrelated pages share it. It is currently empty — the log viewer ships only `patches/admin-status-logs.js`, whose prefix covers both log tabs on every supported release; its CSS is core-page styling and lives in `components/_syslog.css` inside `main.css`, where the `.syslog-view` markup contract is deliberately global so other packages can reuse the viewer. `_`-prefixed files in `patches/` are `@import`-only fragments, never entries.
## JS payloads