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