update 2026-09-20 16:59:13
No files matched your search
@@ -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+
|
||||
|
||||
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 640 KiB |
|
Before Width: | Height: | Size: 550 KiB |
|
Before Width: | Height: | Size: 547 KiB |
|
Before Width: | Height: | Size: 659 KiB |
|
Before Width: | Height: | Size: 650 KiB |
@@ -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
|
||||
end
|
||||
@@ -4,9 +4,50 @@
|
||||
<div class="cbi-section">
|
||||
<h3><%:Events%></h3>
|
||||
<% 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
|
||||
%>
|
||||
|
||||
<div id="content_syslog">
|
||||
<textarea readonly="readonly" wrap="off" rows="<%=self.syslog:cmatch('\n')+2%>" id="syslog"><%=self.syslog:pcdata()%></textarea>
|
||||
<% if log_content == "" then %>
|
||||
<!-- 当没有事件日志时显示提示信息 -->
|
||||
<p style="color: #888; padding: 10px;"><%:No events found in the last 24 hours.%></p>
|
||||
<% elseif string.find(log_content, "^%[ERROR%]") then %>
|
||||
<!-- Docker连接错误:显示红色错误信息 -->
|
||||
<div style="color: #c00; padding: 10px; background: #fee; border: 1px solid #c00; border-radius: 4px;">
|
||||
<%:Docker Error:%> <%=log_content:pcdata()%>
|
||||
</div>
|
||||
<% elseif string.find(log_content, "^%[DEBUG%]") then %>
|
||||
<!-- 调试信息:显示原始响应数据 -->
|
||||
<p style="color: #f90; padding: 10px;"><%:Debug Info - Raw Response:%></p>
|
||||
<textarea readonly="readonly" wrap="off" rows="<%=row_count%>" id="syslog"><%=log_content:pcdata()%></textarea>
|
||||
<% elseif string.find(log_content, "^%[INFO%]") then %>
|
||||
<!-- 信息提示:显示友好提示 -->
|
||||
<p style="color: #888; padding: 10px;"><%:No events found in the last 24 hours.%></p>
|
||||
<% else %>
|
||||
<!-- 正常显示事件日志 -->
|
||||
<textarea readonly="readonly" wrap="off" rows="<%=row_count%>" id="syslog"><%=log_content:pcdata()%></textarea>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if self.title == "Events" then %>
|
||||
</div>
|
||||
<%+footer%>
|
||||
|
||||
@@ -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, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
|
||||
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" })
|
||||
|
||||
@@ -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, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
|
||||
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" })
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, <a target='_blank' href='https://sing-box.sagernet.org/configuration/outbound/urltest'>document</a>"))
|
||||
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" })
|
||||
|
||||
@@ -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, <a target='_blank' href='https://xtls.github.io/config/routing.html#balancerobject'>document</a>"))
|
||||
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" })
|
||||
|
||||
@@ -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 `<page-prefix>.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
|
||||
|
||||