mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 16:57:08 +00:00
refactor: refactor luci interface
This commit is contained in:
parent
409545d0b5
commit
5dfa30cf34
6
build.sh
6
build.sh
@ -98,17 +98,15 @@ ipkg_build=ipkg-build.sh
|
||||
mkdir -p \
|
||||
$opkg_template/usr/bin \
|
||||
$opkg_template/usr/lib/lua/luci/controller \
|
||||
$opkg_template/usr/lib/lua/luci/model/cbi \
|
||||
$opkg_template/usr/lib/lua/luci/model/cbi/ua3f \
|
||||
$opkg_template/usr/lib/lua/luci/view/ua3f \
|
||||
$opkg_template/usr/lib/lua/luci/i18n \
|
||||
$opkg_template/etc/init.d \
|
||||
$opkg_template/etc/config
|
||||
|
||||
cp openwrt/files/luci/controller.lua $opkg_template/usr/lib/lua/luci/controller/ua3f.lua
|
||||
cp openwrt/files/luci/cbi.lua $opkg_template/usr/lib/lua/luci/model/cbi/ua3f.lua
|
||||
cp openwrt/files/luci/statistics.htm $opkg_template/usr/lib/lua/luci/view/ua3f/statistics.htm
|
||||
cp openwrt/files/ua3f.init $opkg_template/etc/init.d/ua3f
|
||||
cp openwrt/files/ua3f.uci $opkg_template/etc/config/ua3f
|
||||
cp -r openwrt/files/luci/* $opkg_template/usr/lib/lua/luci/
|
||||
./po2lmo openwrt/po/zh_cn/ua3f.po $opkg_template/usr/lib/lua/luci/i18n/ua3f.zh-cn.lmo
|
||||
|
||||
# 仅 Linux 平台生成 ipk 包
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
rm -rf /etc/config/ua3f >/dev/null 2>&1
|
||||
rm -rf /var/log/ua3f >/dev/null 2>&1
|
||||
rm -rf /usr/lib/lua/luci/model/cbi/ua3f >/dev/null 2>&1
|
||||
rm -rf /usr/lib/lua/luci/model/cbi/ua3f.lua >/dev/null 2>&1
|
||||
rm -rf /usr/lib/lua/luci/controller/ua3f.lua >/dev/null 2>&1
|
||||
rm -rf /usr/lib/lua/luci/view/ua3f >/dev/null 2>&1
|
||||
|
||||
@ -54,15 +54,9 @@ define Package/ua3f/install
|
||||
$(INSTALL_BIN) ./files/ua3f.init $(1)/etc/init.d/ua3f
|
||||
$(INSTALL_DIR) $(1)/etc/config/
|
||||
$(INSTALL_CONF) ./files/ua3f.uci $(1)/etc/config/ua3f
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/model/cbi/
|
||||
$(INSTALL_CONF) ./files/luci/cbi.lua $(1)/usr/lib/lua/luci/model/cbi/ua3f.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller/
|
||||
$(INSTALL_CONF) ./files/luci/controller.lua $(1)/usr/lib/lua/luci/controller/ua3f.lua
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/ua3f/
|
||||
$(INSTALL_CONF) ./files/luci/statistics.htm $(1)/usr/lib/lua/luci/view/ua3f/statistics.htm
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/ua3f.zh-cn.lmo $(1)/usr/lib/lua/luci/i18n/ua3f.zh-cn.lmo
|
||||
|
||||
$(CP) $(PKG_BUILD_DIR)/files/luci/* $(1)/usr/lib/lua/luci/
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,ua3f))
|
||||
|
||||
@ -1,145 +0,0 @@
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
|
||||
ua3f = Map("ua3f",
|
||||
"UA3F",
|
||||
[[
|
||||
<a href="https://github.com/SunBK201/UA3F" target="_blank">Version: 1.5.0</a>
|
||||
<br>
|
||||
Across the Campus we can reach every corner in the world.
|
||||
]]
|
||||
)
|
||||
|
||||
status = ua3f:section(NamedSection, "enabled", "ua3f", translate("Status"))
|
||||
general = ua3f:section(NamedSection, "main", "ua3f", translate("General"))
|
||||
|
||||
status:option(Flag, "enabled", translate("Enabled"))
|
||||
|
||||
running = status:option(DummyValue, "running", translate("Status"))
|
||||
running.rawhtml = true
|
||||
running.cfgvalue = function(self, section)
|
||||
local pid = luci.sys.exec("pidof ua3f")
|
||||
if pid == "" then
|
||||
return "<input disabled type='button' style='opacity: 1;' class='btn cbi-button cbi-button-reset' value='" ..
|
||||
translate("Stop") .. "'/>"
|
||||
else
|
||||
return "<input disabled type='button' style='opacity: 1;' class='btn cbi-button cbi-button-add' value='" ..
|
||||
translate("Running") .. "'/>"
|
||||
end
|
||||
end
|
||||
|
||||
general:tab("general", translate("Settings"))
|
||||
general:tab("stats", translate("Statistics"))
|
||||
general:tab("log", translate("Log"))
|
||||
general:tab("others", translate("Others Settings"))
|
||||
|
||||
server_mode = general:taboption("general", ListValue, "server_mode", translate("Server Mode"))
|
||||
server_mode:value("HTTP", "HTTP")
|
||||
server_mode:value("SOCKS5", "SOCKS5")
|
||||
server_mode:value("TPROXY", "TPROXY")
|
||||
server_mode:value("REDIRECT", "REDIRECT")
|
||||
server_mode:value("NFQUEUE", "NFQUEUE")
|
||||
|
||||
port = general:taboption("general", Value, "port", translate("Port"))
|
||||
port.placeholder = "1080"
|
||||
|
||||
bind = general:taboption("general", Value, "bind", translate("Bind Address"))
|
||||
bind:value("127.0.0.1")
|
||||
bind:value("0.0.0.0")
|
||||
|
||||
log_level = general:taboption("general", ListValue, "log_level", translate("Log Level"))
|
||||
log_level:value("debug")
|
||||
log_level:value("info")
|
||||
log_level:value("warn")
|
||||
log_level:value("error")
|
||||
log_level:value("fatal")
|
||||
log_level:value("panic")
|
||||
log_level.description = translate(
|
||||
"Sets the logging level. Do not keep the log level set to debug/info/warn for an extended period of time.")
|
||||
|
||||
ua = general:taboption("general", Value, "ua", translate("User-Agent"))
|
||||
ua.placeholder = "FFF"
|
||||
ua.description = translate("User-Agent after rewrite")
|
||||
|
||||
uaRegexPattern = general:taboption("general", Value, "ua_regex", translate("User-Agent Regex Pattern"))
|
||||
uaRegexPattern.description = translate("Regular expression pattern for matching User-Agent")
|
||||
|
||||
partialRepalce = general:taboption("general", Flag, "partial_replace", translate("Partial Replace"))
|
||||
partialRepalce.description =
|
||||
translate("Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty")
|
||||
partialRepalce.default = "0"
|
||||
|
||||
directForward = general:taboption("general", Flag, "direct_forward", translate("Direct Forward"))
|
||||
directForward.description =
|
||||
translate("Directly forward packets without rewriting")
|
||||
directForward.default = "0"
|
||||
|
||||
log = general:taboption("log", TextValue, "log")
|
||||
log.readonly = true
|
||||
log.rows = 30
|
||||
function log.cfgvalue(self, section)
|
||||
local logfile = "/var/log/ua3f/ua3f.log"
|
||||
local fs = require("nixio.fs")
|
||||
if not fs.access(logfile) then
|
||||
return ""
|
||||
end
|
||||
local n = tonumber(luci.model.uci.cursor():get("ua3f", section, "log_lines")) or 1000
|
||||
return luci.sys.exec("tail -n " .. n .. " " .. logfile)
|
||||
end
|
||||
|
||||
function log.write(self, section, value) end
|
||||
|
||||
function log.render(self, section, scope)
|
||||
TextValue.render(self, section, scope)
|
||||
luci.http.write("<script>")
|
||||
luci.http.write([[
|
||||
var textarea = document.getElementById('cbid.ua3f.main.log');
|
||||
if (textarea) {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
]])
|
||||
luci.http.write("</script>")
|
||||
end
|
||||
|
||||
logLines = general:taboption("log", Value, "log_lines", translate("Display Lines"))
|
||||
logLines.default = "1000"
|
||||
logLines.datatype = "uinteger"
|
||||
logLines.rmempty = false
|
||||
|
||||
clearlog = general:taboption("log", Button, "_clearlog", translate("Clear Logs"))
|
||||
clearlog.inputtitle = translate("Clear Logs")
|
||||
clearlog.inputstyle = "reset"
|
||||
function clearlog.write(self, section)
|
||||
end
|
||||
|
||||
function clearlog.render(self, section, scope)
|
||||
Button.render(self, section, scope)
|
||||
luci.http.write([[
|
||||
<script>
|
||||
document.querySelector("input[name='cbid.ua3f.main._clearlog']").addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
fetch(']] .. luci.dispatcher.build_url("admin/services/ua3f/clear_log") .. [[', {method: 'POST'})
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
var textarea = document.getElementById('cbid.ua3f.main.log');
|
||||
if (textarea) textarea.value = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
]])
|
||||
end
|
||||
|
||||
download = general:taboption("log", Button, "_download", translate("Download Logs"))
|
||||
download.inputtitle = translate("Download Logs")
|
||||
download.inputstyle = "apply"
|
||||
function download.write(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/services/ua3f/download_log"))
|
||||
end
|
||||
|
||||
stats = general:taboption("stats", DummyValue, "")
|
||||
stats.template = "ua3f/statistics"
|
||||
|
||||
ttl = general:taboption("others", Flag, "set_ttl", translate("Set TTL"))
|
||||
ttl.description = translate("Set the TTL 64 for packets")
|
||||
|
||||
return ua3f
|
||||
@ -6,27 +6,30 @@ function index()
|
||||
entry({ "admin", "services", "ua3f", "clear_log" }, call("clear_log")).leaf = true
|
||||
end
|
||||
|
||||
function action_download_log()
|
||||
local nixio = require "nixio"
|
||||
local fs = require "nixio.fs"
|
||||
local http = require "luci.http"
|
||||
local tmpfile = "/tmp/ua3f_logs.tar.gz"
|
||||
local fs = require("nixio.fs")
|
||||
|
||||
function create_log_archive()
|
||||
local tmpfile = "/tmp/ua3f_logs.tar.gz"
|
||||
local cmd = "cd /var/log && tar -czf " .. tmpfile .. " ua3f >/dev/null 2>&1"
|
||||
os.execute(cmd)
|
||||
return tmpfile
|
||||
end
|
||||
|
||||
if not fs.access(tmpfile) then
|
||||
function send_file_download(filepath, filename)
|
||||
local http = require("luci.http")
|
||||
|
||||
if not fs.access(filepath) then
|
||||
http.status(500, "Internal Server Error")
|
||||
http.prepare_content("text/plain")
|
||||
http.write("Failed to create archive")
|
||||
return
|
||||
return false
|
||||
end
|
||||
|
||||
http.header("Content-Type", "application/gzip")
|
||||
http.header("Content-Disposition", 'attachment; filename="ua3f_logs.tar.gz"')
|
||||
http.header("Content-Length", tostring(fs.stat(tmpfile).size))
|
||||
http.header("Content-Disposition", 'attachment; filename="' .. filename .. '"')
|
||||
http.header("Content-Length", tostring(fs.stat(filepath).size))
|
||||
|
||||
local fp = io.open(tmpfile, "rb")
|
||||
local fp = io.open(filepath, "rb")
|
||||
if fp then
|
||||
while true do
|
||||
local chunk = fp:read(2048)
|
||||
@ -36,15 +39,35 @@ function action_download_log()
|
||||
fp:close()
|
||||
end
|
||||
|
||||
nixio.fs.remove(tmpfile)
|
||||
return true
|
||||
end
|
||||
|
||||
function clear_log_file(logfile)
|
||||
if fs.access(logfile) then
|
||||
fs.writefile(logfile, "")
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function action_download_log()
|
||||
local tmpfile = create_log_archive()
|
||||
local success = send_file_download(tmpfile, "ua3f_logs.tar.gz")
|
||||
if success then
|
||||
fs.remove(tmpfile)
|
||||
end
|
||||
end
|
||||
|
||||
function clear_log()
|
||||
local logfile = "/var/log/ua3f/ua3f.log"
|
||||
local fs = require "nixio.fs"
|
||||
if fs.access(logfile) then
|
||||
fs.writefile(logfile, "")
|
||||
local success = clear_log_file(logfile)
|
||||
|
||||
local http = luci.http
|
||||
if success then
|
||||
http.status(200, "OK")
|
||||
http.write("Log cleared")
|
||||
else
|
||||
http.status(404, "Not Found")
|
||||
http.write("Log file not found")
|
||||
end
|
||||
luci.http.status(200, "OK")
|
||||
luci.http.write("Log cleared")
|
||||
end
|
||||
40
openwrt/files/luci/model/cbi/ua3f.lua
Normal file
40
openwrt/files/luci/model/cbi/ua3f.lua
Normal file
@ -0,0 +1,40 @@
|
||||
local cbi = require("luci.cbi")
|
||||
local i18n = require("luci.i18n")
|
||||
local translate = i18n.translate
|
||||
local NamedSection = cbi.NamedSection
|
||||
|
||||
local ua3f = cbi.Map("ua3f",
|
||||
"UA3F",
|
||||
[[
|
||||
<a href="https://github.com/SunBK201/UA3F" target="_blank">Version: 1.5.0</a>
|
||||
<br>
|
||||
Across the Campus we can reach every corner in the world.
|
||||
]]
|
||||
)
|
||||
local fields = require("luci.model.cbi.ua3f.fields")
|
||||
|
||||
function create_sections(map)
|
||||
local sections = {}
|
||||
|
||||
-- Status Section
|
||||
sections.status = map:section(NamedSection, "enabled", "ua3f", translate("Status"))
|
||||
|
||||
-- General Section with tabs
|
||||
sections.general = map:section(NamedSection, "main", "ua3f", translate("General"))
|
||||
sections.general:tab("general", translate("Settings"))
|
||||
sections.general:tab("stats", translate("Statistics"))
|
||||
sections.general:tab("log", translate("Log"))
|
||||
sections.general:tab("others", translate("Others Settings"))
|
||||
|
||||
return sections
|
||||
end
|
||||
|
||||
local sections = create_sections(ua3f)
|
||||
|
||||
fields.add_status_fields(sections.status)
|
||||
fields.add_general_fields(sections.general)
|
||||
fields.add_stats_fields(sections.general)
|
||||
fields.add_log_fields(sections.general)
|
||||
fields.add_others_fields(sections.general)
|
||||
|
||||
return ua3f
|
||||
173
openwrt/files/luci/model/cbi/ua3f/fields.lua
Normal file
173
openwrt/files/luci/model/cbi/ua3f/fields.lua
Normal file
@ -0,0 +1,173 @@
|
||||
local M = {}
|
||||
|
||||
local cbi = require("luci.cbi")
|
||||
local i18n = require("luci.i18n")
|
||||
local sys = require("luci.sys")
|
||||
local translate = i18n.translate
|
||||
|
||||
local Flag = cbi.Flag
|
||||
local Value = cbi.Value
|
||||
local ListValue = cbi.ListValue
|
||||
local DummyValue = cbi.DummyValue
|
||||
local TextValue = cbi.TextValue
|
||||
local Button = cbi.Button
|
||||
|
||||
-- Status Section Fields
|
||||
function M.add_status_fields(section)
|
||||
-- Enabled Flag
|
||||
section:option(Flag, "enabled", translate("Enabled"))
|
||||
|
||||
-- Running Status Display
|
||||
local running = section:option(DummyValue, "running", translate("Status"))
|
||||
running.rawhtml = true
|
||||
running.cfgvalue = function(self, section)
|
||||
local pid = sys.exec("pidof ua3f")
|
||||
if pid == "" then
|
||||
return "<input disabled type='button' style='opacity: 1;' class='btn cbi-button cbi-button-reset' value='" ..
|
||||
translate("Stop") .. "'/>"
|
||||
else
|
||||
return "<input disabled type='button' style='opacity: 1;' class='btn cbi-button cbi-button-add' value='" ..
|
||||
translate("Running") .. "'/>"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- General Tab Fields
|
||||
function M.add_general_fields(section)
|
||||
-- Server Mode
|
||||
local server_mode = section:taboption("general", ListValue, "server_mode", translate("Server Mode"))
|
||||
server_mode:value("HTTP", "HTTP")
|
||||
server_mode:value("SOCKS5", "SOCKS5")
|
||||
server_mode:value("TPROXY", "TPROXY")
|
||||
server_mode:value("REDIRECT", "REDIRECT")
|
||||
server_mode:value("NFQUEUE", "NFQUEUE")
|
||||
|
||||
-- Port
|
||||
local port = section:taboption("general", Value, "port", translate("Port"))
|
||||
port.placeholder = "1080"
|
||||
|
||||
-- Bind Address
|
||||
local bind = section:taboption("general", Value, "bind", translate("Bind Address"))
|
||||
bind:value("127.0.0.1")
|
||||
bind:value("0.0.0.0")
|
||||
|
||||
-- Log Level
|
||||
local log_level = section:taboption("general", ListValue, "log_level", translate("Log Level"))
|
||||
log_level:value("debug")
|
||||
log_level:value("info")
|
||||
log_level:value("warn")
|
||||
log_level:value("error")
|
||||
log_level:value("fatal")
|
||||
log_level:value("panic")
|
||||
log_level.description = translate(
|
||||
"Sets the logging level. Do not keep the log level set to debug/info/warn for an extended period of time.")
|
||||
|
||||
-- User-Agent
|
||||
local ua = section:taboption("general", Value, "ua", translate("User-Agent"))
|
||||
ua.placeholder = "FFF"
|
||||
ua.description = translate("User-Agent after rewrite")
|
||||
|
||||
-- User-Agent Regex
|
||||
local uaRegexPattern = section:taboption("general", Value, "ua_regex", translate("User-Agent Regex"))
|
||||
uaRegexPattern.description = translate("Regular expression pattern for matching User-Agent")
|
||||
|
||||
-- Partial Replace
|
||||
local partialReplace = section:taboption("general", Flag, "partial_replace", translate("Partial Replace"))
|
||||
partialReplace.description =
|
||||
translate(
|
||||
"Replace only the matched part of the User-Agent, only works when User-Agent Regex is not empty")
|
||||
partialReplace.default = "0"
|
||||
|
||||
-- Direct Forward
|
||||
local directForward = section:taboption("general", Flag, "direct_forward", translate("Direct Forward"))
|
||||
directForward.description =
|
||||
translate("Directly forward packets without rewriting")
|
||||
directForward.default = "0"
|
||||
end
|
||||
|
||||
-- Statistics Tab Fields
|
||||
function M.add_stats_fields(section)
|
||||
local stats = section:taboption("stats", DummyValue, "")
|
||||
stats.template = "ua3f/statistics"
|
||||
end
|
||||
|
||||
-- Log Tab Fields
|
||||
function M.add_log_fields(section)
|
||||
-- Log Display
|
||||
local log = section:taboption("log", TextValue, "log")
|
||||
log.readonly = true
|
||||
log.rows = 30
|
||||
function log.cfgvalue(self, section)
|
||||
local logfile = "/var/log/ua3f/ua3f.log"
|
||||
local fs = require("nixio.fs")
|
||||
if not fs.access(logfile) then
|
||||
return ""
|
||||
end
|
||||
local n = tonumber(luci.model.uci.cursor():get("ua3f", section, "log_lines")) or 1000
|
||||
return luci.sys.exec("tail -n " .. n .. " " .. logfile)
|
||||
end
|
||||
|
||||
function log.write(self, section, value) end
|
||||
|
||||
function log.render(self, section, scope)
|
||||
TextValue.render(self, section, scope)
|
||||
luci.http.write("<script>")
|
||||
luci.http.write([[
|
||||
var textarea = document.getElementById('cbid.ua3f.main.log');
|
||||
if (textarea) {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}
|
||||
]])
|
||||
luci.http.write("</script>")
|
||||
end
|
||||
|
||||
-- Log Lines
|
||||
local logLines = section:taboption("log", Value, "log_lines", translate("Display Lines"))
|
||||
logLines.default = "1000"
|
||||
logLines.datatype = "uinteger"
|
||||
logLines.rmempty = false
|
||||
|
||||
-- Clear Log Button
|
||||
local clearlog = section:taboption("log", Button, "_clearlog", translate("Clear Logs"))
|
||||
clearlog.inputtitle = translate("Clear Logs")
|
||||
clearlog.inputstyle = "reset"
|
||||
function clearlog.write(self, section)
|
||||
end
|
||||
|
||||
function clearlog.render(self, section, scope)
|
||||
Button.render(self, section, scope)
|
||||
luci.http.write([[
|
||||
<script>
|
||||
document.querySelector("input[name='cbid.ua3f.main._clearlog']").addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
fetch(']] .. luci.dispatcher.build_url("admin/services/ua3f/clear_log") .. [[', {method: 'POST'})
|
||||
.then(resp => {
|
||||
if (resp.ok) {
|
||||
var textarea = document.getElementById('cbid.ua3f.main.log');
|
||||
if (textarea) textarea.value = "";
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
]])
|
||||
end
|
||||
|
||||
-- Download Log Button
|
||||
local download = section:taboption("log", Button, "_download", translate("Download Logs"))
|
||||
download.inputtitle = translate("Download Logs")
|
||||
download.inputstyle = "apply"
|
||||
function download.write(self, section)
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin/services/ua3f/download_log"))
|
||||
end
|
||||
|
||||
return log
|
||||
end
|
||||
|
||||
-- Others Tab Fields
|
||||
function M.add_others_fields(section)
|
||||
-- TTL Setting
|
||||
local ttl = section:taboption("others", Flag, "set_ttl", translate("Set TTL"))
|
||||
ttl.description = translate("Set the TTL 64 for packets")
|
||||
end
|
||||
|
||||
return M
|
||||
72
openwrt/files/luci/model/cbi/ua3f/statistics.lua
Normal file
72
openwrt/files/luci/model/cbi/ua3f/statistics.lua
Normal file
@ -0,0 +1,72 @@
|
||||
-- UA3F Statistics Data Module
|
||||
local M = {}
|
||||
|
||||
-- Read rewrite statistics from file
|
||||
function M.read_rewrite_stats()
|
||||
local stats = {}
|
||||
local file = io.open("/var/log/ua3f/rewrite_stats", "r")
|
||||
if file then
|
||||
for line in file:lines() do
|
||||
local host, count, origin_ua, mocked_ua = line:match("^(%S+)%s+(%d+)%s+(.-)SEQSEQ(.-)%s*$")
|
||||
if host and count then
|
||||
table.insert(stats, {
|
||||
host = host,
|
||||
count = count,
|
||||
origin_ua = origin_ua,
|
||||
mocked_ua = mocked_ua
|
||||
})
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
return stats
|
||||
end
|
||||
|
||||
-- Read pass-through statistics from file
|
||||
function M.read_pass_stats()
|
||||
local stats = {}
|
||||
local file = io.open("/var/log/ua3f/pass_stats", "r")
|
||||
if file then
|
||||
for line in file:lines() do
|
||||
local srcAddr, destAddr, count, ua = line:match("^(%S+)%s(%S+)%s(%d+)%s(.+)$")
|
||||
if ua and count then
|
||||
table.insert(stats, {
|
||||
ua = ua,
|
||||
count = count,
|
||||
srcAddr = srcAddr,
|
||||
destAddr = destAddr
|
||||
})
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
return stats
|
||||
end
|
||||
|
||||
-- Read connection statistics from file
|
||||
function M.read_conn_stats()
|
||||
local stats = {}
|
||||
local file = io.open("/var/log/ua3f/conn_stats", "r")
|
||||
if file then
|
||||
for line in file:lines() do
|
||||
local protocol, srcAddr, destAddr, duration = line:match("^(%S+)%s(%S+)%s(%S+)%s(.+)$")
|
||||
if protocol and srcAddr and destAddr and duration then
|
||||
table.insert(stats, {
|
||||
protocol = protocol,
|
||||
srcAddr = srcAddr,
|
||||
destAddr = destAddr,
|
||||
duration = duration
|
||||
})
|
||||
end
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
return stats
|
||||
end
|
||||
|
||||
-- Generate row style class
|
||||
function M.rowstyle(i)
|
||||
return (i % 2 == 0) and "cbi-rowstyle-2" or "cbi-rowstyle-1"
|
||||
end
|
||||
|
||||
return M
|
||||
@ -1,427 +0,0 @@
|
||||
<%
|
||||
local rewrite_stats = {}
|
||||
local rewrite_stats_file = io.open("/var/log/ua3f/rewrite_stats", "r")
|
||||
if rewrite_stats_file then
|
||||
for line in rewrite_stats_file:lines() do
|
||||
local host, count, origin_ua, mocked_ua = line:match("^(%S+)%s+(%d+)%s+(.-)SEQSEQ(.-)%s*$")
|
||||
if host and count then
|
||||
table.insert(rewrite_stats, {host = host, count = count, origin_ua = origin_ua, mocked_ua = mocked_ua})
|
||||
end
|
||||
end
|
||||
rewrite_stats_file:close()
|
||||
end
|
||||
|
||||
local pass_stats = {}
|
||||
local pass_stats_file = io.open("/var/log/ua3f/pass_stats", "r")
|
||||
if pass_stats_file then
|
||||
for line in pass_stats_file:lines() do
|
||||
local srcAddr, destAddr, count, ua = line:match("^(%S+)%s(%S+)%s(%d+)%s(.+)$")
|
||||
if ua and count then
|
||||
table.insert(pass_stats, {ua = ua, count = count, srcAddr = srcAddr, destAddr = destAddr})
|
||||
end
|
||||
end
|
||||
pass_stats_file:close()
|
||||
end
|
||||
|
||||
local conn_stats = {}
|
||||
local conn_stats_file = io.open("/var/log/ua3f/conn_stats", "r")
|
||||
if conn_stats_file then
|
||||
for line in conn_stats_file:lines() do
|
||||
local protocol, srcAddr, destAddr, duration = line:match("^(%S+)%s(%S+)%s(%S+)%s(.+)$")
|
||||
if protocol and srcAddr and destAddr and duration then
|
||||
table.insert(conn_stats, {protocol = protocol, srcAddr = srcAddr, destAddr = destAddr, duration = duration})
|
||||
end
|
||||
end
|
||||
conn_stats_file:close()
|
||||
end
|
||||
|
||||
local function rowstyle(i)
|
||||
return (i % 2 == 0) and "cbi-rowstyle-2" or "cbi-rowstyle-1"
|
||||
end
|
||||
%>
|
||||
|
||||
<h3><%:Statistics%></h3>
|
||||
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:User-Agent Rewrite Statistics%></div>
|
||||
<table id="rewrite-stats-table" class="table cbi-section-table">
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:Host%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Rewrite Count%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Original User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Modified User-Agent%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(rewrite_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:Host%>"><span><%= item.host %></span></td>
|
||||
<td class="td" data-title="<%:Rewrite Count%>"><%= item.count %></td>
|
||||
<td class="td" data-title="<%:Original User-Agent%>"><span><%= item.origin_ua %></span></td>
|
||||
<td class="td" data-title="<%:Modified User-Agent%>"><span><%= item.mocked_ua %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:User-Agent Pass-Through Statistics%></div>
|
||||
<table id="pass-stats-table" class="table cbi-section-table">
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Pass-Through Count%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Last Source Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Last Destination Address%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(pass_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:User-Agent%>"><span><%= item.ua %></span></td>
|
||||
<td class="td" data-title="<%:Pass-Through Count%>"><%= item.count %></td>
|
||||
<td class="td" data-title="<%:Last Source Address%>"><span><%= item.srcAddr %></span></td>
|
||||
<td class="td" data-title="<%:Last Destination Address%>"><span><%= item.destAddr %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:Connection Statistics%></div>
|
||||
<table id="conn-stats-table" class="table cbi-section-table">
|
||||
<tr>
|
||||
<td colspan="4" style="padding:5px 0;">
|
||||
<%:Total Connections%>: <%= #conn_stats %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:Protocol%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Source Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Destination Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Duration%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(conn_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:Protocol%>"><span><%= item.protocol %></span></td>
|
||||
<td class="td" data-title="<%:Source Address%>"><span><%= item.srcAddr %></span></td>
|
||||
<td class="td" data-title="<%:Destination Address%>"><span><%= item.destAddr %></span></td>
|
||||
<td class="td" data-title="<%:Duration%>" data-seconds="<%= item.duration %>"><span class="duration-text"><%= item.duration %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
function formatDuration(seconds) {
|
||||
seconds = parseInt(seconds);
|
||||
if (isNaN(seconds) || seconds < 0) return '0s';
|
||||
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(days + 'd');
|
||||
if (hours > 0) parts.push(hours + 'h');
|
||||
if (minutes > 0) parts.push(minutes + 'm');
|
||||
if (secs > 0 || parts.length === 0) parts.push(secs + 's');
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function formatAllDurations() {
|
||||
const durationCells = document.querySelectorAll('#conn-stats-table td[data-seconds]');
|
||||
durationCells.forEach(cell => {
|
||||
const seconds = cell.getAttribute('data-seconds');
|
||||
const textSpan = cell.querySelector('.duration-text');
|
||||
if (textSpan && seconds) {
|
||||
textSpan.textContent = formatDuration(seconds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sortTable(tableId, columnIndex, isNumeric) {
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table.querySelector('tbody') || table;
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.tr')).filter(row => !row.classList.contains('table-titles'));
|
||||
|
||||
const sortKey = `${tableId}-sort`;
|
||||
let sortState = sessionStorage.getItem(sortKey);
|
||||
let currentSort = sortState ? JSON.parse(sortState) : {column: -1, order: 'asc'};
|
||||
|
||||
let order = 'asc';
|
||||
if (currentSort.column === columnIndex) {
|
||||
order = currentSort.order === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
if (tableId === 'conn-stats-table' && columnIndex === 3) {
|
||||
aVal = parseFloat(a.children[columnIndex].getAttribute('data-seconds')) || 0;
|
||||
bVal = parseFloat(b.children[columnIndex].getAttribute('data-seconds')) || 0;
|
||||
} else {
|
||||
aVal = a.children[columnIndex].textContent.trim();
|
||||
bVal = b.children[columnIndex].textContent.trim();
|
||||
|
||||
if (isNumeric) {
|
||||
aVal = parseFloat(aVal) || 0;
|
||||
bVal = parseFloat(bVal) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
row.className = row.className.replace(/cbi-rowstyle-[12]/, (index % 2 === 0) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1');
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
sessionStorage.setItem(sortKey, JSON.stringify({column: columnIndex, order: order}));
|
||||
|
||||
updateSortIndicators(tableId, columnIndex, order);
|
||||
}
|
||||
|
||||
function updateSortIndicators(tableId, activeColumn, order) {
|
||||
const table = document.getElementById(tableId);
|
||||
const headers = table.querySelectorAll('th[data-sortable-row="true"]');
|
||||
|
||||
headers.forEach((th, index) => {
|
||||
th.style.position = 'relative';
|
||||
th.style.cursor = 'pointer';
|
||||
let indicator = th.querySelector('.sort-indicator');
|
||||
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('span');
|
||||
indicator.className = 'sort-indicator';
|
||||
indicator.style.fontSize = '0.8em';
|
||||
indicator.style.marginLeft = '4px';
|
||||
indicator.style.display = 'inline-block';
|
||||
indicator.style.width = '12px';
|
||||
indicator.style.textAlign = 'center';
|
||||
th.appendChild(indicator);
|
||||
}
|
||||
|
||||
if (index === activeColumn) {
|
||||
indicator.textContent = order === 'asc' ? '▲' : '▼';
|
||||
indicator.style.visibility = 'visible';
|
||||
} else {
|
||||
indicator.textContent = '';
|
||||
indicator.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function restoreTableSort(tableId, numericColumns) {
|
||||
const sortKey = `${tableId}-sort`;
|
||||
const sortState = sessionStorage.getItem(sortKey);
|
||||
|
||||
if (sortState) {
|
||||
const {column, order} = JSON.parse(sortState);
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table.querySelector('tbody') || table;
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.tr')).filter(row => !row.classList.contains('table-titles'));
|
||||
|
||||
if (rows.length > 0) {
|
||||
const isNumeric = numericColumns.includes(column);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
if (tableId === 'conn-stats-table' && column === 3) {
|
||||
aVal = parseFloat(a.children[column].getAttribute('data-seconds')) || 0;
|
||||
bVal = parseFloat(b.children[column].getAttribute('data-seconds')) || 0;
|
||||
} else {
|
||||
aVal = a.children[column].textContent.trim();
|
||||
bVal = b.children[column].textContent.trim();
|
||||
|
||||
if (isNumeric) {
|
||||
aVal = parseFloat(aVal) || 0;
|
||||
bVal = parseFloat(bVal) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
row.className = row.className.replace(/cbi-rowstyle-[12]/, (index % 2 === 0) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1');
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
updateSortIndicators(tableId, column, order);
|
||||
}
|
||||
}
|
||||
|
||||
function bindTableEvents(tableId, numericColumns) {
|
||||
const table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
|
||||
const headers = table.querySelectorAll('th[data-sortable-row="true"]');
|
||||
headers.forEach((th, index) => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.style.userSelect = 'none';
|
||||
|
||||
const newTh = th.cloneNode(true);
|
||||
th.parentNode.replaceChild(newTh, th);
|
||||
|
||||
if (!newTh.querySelector('.sort-indicator')) {
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = 'sort-indicator';
|
||||
indicator.style.fontSize = '0.8em';
|
||||
indicator.style.marginLeft = '4px';
|
||||
indicator.style.display = 'inline-block';
|
||||
indicator.style.width = '12px';
|
||||
indicator.style.textAlign = 'center';
|
||||
indicator.style.visibility = 'hidden';
|
||||
indicator.textContent = '';
|
||||
newTh.appendChild(indicator);
|
||||
}
|
||||
|
||||
newTh.addEventListener('click', () => {
|
||||
const isNumeric = numericColumns.includes(index);
|
||||
sortTable(tableId, index, isNumeric);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initTableSort() {
|
||||
const tables = [
|
||||
{id: 'rewrite-stats-table', numericColumns: [1]},
|
||||
{id: 'pass-stats-table', numericColumns: [1]},
|
||||
{id: 'conn-stats-table', numericColumns: [3]}
|
||||
];
|
||||
|
||||
formatAllDurations();
|
||||
|
||||
tables.forEach(tableInfo => {
|
||||
bindTableEvents(tableInfo.id, tableInfo.numericColumns);
|
||||
restoreTableSort(tableInfo.id, tableInfo.numericColumns);
|
||||
});
|
||||
}
|
||||
|
||||
async function updateStats() {
|
||||
try {
|
||||
const response = await fetch(window.location.href, {cache: "no-store"});
|
||||
const text = await response.text();
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, "text/html");
|
||||
const newRewriteTable = doc.querySelector("#rewrite-stats-table");
|
||||
const newPassTable = doc.querySelector("#pass-stats-table");
|
||||
const newConnTable = doc.querySelector("#conn-stats-table");
|
||||
const newLog = doc.querySelector("#cbid\\.ua3f\\.main\\.log");
|
||||
|
||||
if (newRewriteTable) {
|
||||
document.querySelector("#rewrite-stats-table").innerHTML = newRewriteTable.innerHTML;
|
||||
bindTableEvents('rewrite-stats-table', [1]);
|
||||
restoreTableSort('rewrite-stats-table', [1]);
|
||||
}
|
||||
if (newPassTable) {
|
||||
document.querySelector("#pass-stats-table").innerHTML = newPassTable.innerHTML;
|
||||
bindTableEvents('pass-stats-table', [1]);
|
||||
restoreTableSort('pass-stats-table', [1]);
|
||||
}
|
||||
if (newConnTable) {
|
||||
document.querySelector("#conn-stats-table").innerHTML = newConnTable.innerHTML;
|
||||
formatAllDurations();
|
||||
bindTableEvents('conn-stats-table', [3]);
|
||||
restoreTableSort('conn-stats-table', [3]);
|
||||
}
|
||||
if (newLog) {
|
||||
document.querySelector("#cbid\\.ua3f\\.main\\.log").value = newLog.value;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("update stats error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTableSort);
|
||||
} else {
|
||||
initTableSort();
|
||||
}
|
||||
|
||||
setInterval(updateStats, 5000);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
th[data-sortable-row="true"] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
th[data-sortable-row="true"]:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
color: #0066cc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(1),
|
||||
#rewrite-stats-table td:nth-child(1) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(2),
|
||||
#rewrite-stats-table td:nth-child(2) {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(3),
|
||||
#rewrite-stats-table td:nth-child(3) {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(4),
|
||||
#rewrite-stats-table td:nth-child(4) {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(1),
|
||||
#pass-stats-table td:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(2),
|
||||
#pass-stats-table td:nth-child(2) {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(3),
|
||||
#pass-stats-table td:nth-child(3) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(4),
|
||||
#pass-stats-table td:nth-child(4) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(1),
|
||||
#conn-stats-table td:nth-child(1) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(2),
|
||||
#conn-stats-table td:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(3),
|
||||
#conn-stats-table td:nth-child(3) {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(4),
|
||||
#conn-stats-table td:nth-child(4) {
|
||||
width: 15%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
410
openwrt/files/luci/view/ua3f/statistics.htm
Normal file
410
openwrt/files/luci/view/ua3f/statistics.htm
Normal file
@ -0,0 +1,410 @@
|
||||
<%
|
||||
local data = require("luci.model.cbi.ua3f.statistics")
|
||||
|
||||
local rewrite_stats = data.read_rewrite_stats()
|
||||
local pass_stats = data.read_pass_stats()
|
||||
local conn_stats = data.read_conn_stats()
|
||||
local rowstyle = data.rowstyle
|
||||
%>
|
||||
|
||||
<h3><%:Statistics%></h3>
|
||||
|
||||
<!-- Rewrite Statistics Table -->
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:User-Agent Rewrite Statistics%></div>
|
||||
<table id="rewrite-stats-table" class="table cbi-section-table">
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:Host%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Rewrite Count%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Original User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Modified User-Agent%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(rewrite_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:Host%>"><span><%= item.host %></span></td>
|
||||
<td class="td" data-title="<%:Rewrite Count%>"><%= item.count %></td>
|
||||
<td class="td" data-title="<%:Original User-Agent%>"><span><%= item.origin_ua %></span></td>
|
||||
<td class="td" data-title="<%:Modified User-Agent%>"><span><%= item.mocked_ua %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<!-- Pass-Through Statistics Table -->
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:User-Agent Pass-Through Statistics%></div>
|
||||
<table id="pass-stats-table" class="table cbi-section-table">
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:User-Agent%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Pass-Through Count%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Last Source Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Last Destination Address%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(pass_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:User-Agent%>"><span><%= item.ua %></span></td>
|
||||
<td class="td" data-title="<%:Pass-Through Count%>"><%= item.count %></td>
|
||||
<td class="td" data-title="<%:Last Source Address%>"><span><%= item.srcAddr %></span></td>
|
||||
<td class="td" data-title="<%:Last Destination Address%>"><span><%= item.destAddr %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<!-- Connection Statistics Table -->
|
||||
<div class="cbi-section-descr" style="font-weight:bold;"><%:Connection Statistics%></div>
|
||||
<table id="conn-stats-table" class="table cbi-section-table">
|
||||
<tr>
|
||||
<td colspan="4" style="padding:5px 0;">
|
||||
<%:Total Connections%>: <%= #conn_stats %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="tr table-titles">
|
||||
<th class="th" data-sortable-row="true"><%:Protocol%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Source Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Destination Address%></th>
|
||||
<th class="th" data-sortable-row="true"><%:Duration%></th>
|
||||
</tr>
|
||||
|
||||
<% for i, item in ipairs(conn_stats) do %>
|
||||
<tr class="tr <%= rowstyle(i) %>">
|
||||
<td class="td" data-title="<%:Protocol%>"><span><%= item.protocol %></span></td>
|
||||
<td class="td" data-title="<%:Source Address%>"><span><%= item.srcAddr %></span></td>
|
||||
<td class="td" data-title="<%:Destination Address%>"><span><%= item.destAddr %></span></td>
|
||||
<td class="td" data-title="<%:Duration%>" data-seconds="<%= item.duration %>"><span class="duration-text"><%= item.duration %></span></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<script type="text/javascript">
|
||||
// Format duration in seconds to human-readable format
|
||||
function formatDuration(seconds) {
|
||||
seconds = parseInt(seconds);
|
||||
if (isNaN(seconds) || seconds < 0) return '0s';
|
||||
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(days + 'd');
|
||||
if (hours > 0) parts.push(hours + 'h');
|
||||
if (minutes > 0) parts.push(minutes + 'm');
|
||||
if (secs > 0 || parts.length === 0) parts.push(secs + 's');
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
// Format all duration cells in the connection statistics table
|
||||
function formatAllDurations() {
|
||||
const durationCells = document.querySelectorAll('#conn-stats-table td[data-seconds]');
|
||||
durationCells.forEach(cell => {
|
||||
const seconds = cell.getAttribute('data-seconds');
|
||||
const textSpan = cell.querySelector('.duration-text');
|
||||
if (textSpan && seconds) {
|
||||
textSpan.textContent = formatDuration(seconds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Sort table by column
|
||||
function sortTable(tableId, columnIndex, isNumeric) {
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table.querySelector('tbody') || table;
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.tr')).filter(row => !row.classList.contains('table-titles'));
|
||||
|
||||
const sortKey = `${tableId}-sort`;
|
||||
let sortState = sessionStorage.getItem(sortKey);
|
||||
let currentSort = sortState ? JSON.parse(sortState) : {column: -1, order: 'asc'};
|
||||
|
||||
let order = 'asc';
|
||||
if (currentSort.column === columnIndex) {
|
||||
order = currentSort.order === 'asc' ? 'desc' : 'asc';
|
||||
}
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
if (tableId === 'conn-stats-table' && columnIndex === 3) {
|
||||
aVal = parseFloat(a.children[columnIndex].getAttribute('data-seconds')) || 0;
|
||||
bVal = parseFloat(b.children[columnIndex].getAttribute('data-seconds')) || 0;
|
||||
} else {
|
||||
aVal = a.children[columnIndex].textContent.trim();
|
||||
bVal = b.children[columnIndex].textContent.trim();
|
||||
|
||||
if (isNumeric) {
|
||||
aVal = parseFloat(aVal) || 0;
|
||||
bVal = parseFloat(bVal) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
row.className = row.className.replace(/cbi-rowstyle-[12]/, (index % 2 === 0) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1');
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
sessionStorage.setItem(sortKey, JSON.stringify({column: columnIndex, order: order}));
|
||||
|
||||
updateSortIndicators(tableId, columnIndex, order);
|
||||
}
|
||||
|
||||
// Update sort indicators on table headers
|
||||
function updateSortIndicators(tableId, activeColumn, order) {
|
||||
const table = document.getElementById(tableId);
|
||||
const headers = table.querySelectorAll('th[data-sortable-row="true"]');
|
||||
|
||||
headers.forEach((th, index) => {
|
||||
th.style.position = 'relative';
|
||||
th.style.cursor = 'pointer';
|
||||
let indicator = th.querySelector('.sort-indicator');
|
||||
|
||||
if (!indicator) {
|
||||
indicator = document.createElement('span');
|
||||
indicator.className = 'sort-indicator';
|
||||
indicator.style.fontSize = '0.8em';
|
||||
indicator.style.marginLeft = '4px';
|
||||
indicator.style.display = 'inline-block';
|
||||
indicator.style.width = '12px';
|
||||
indicator.style.textAlign = 'center';
|
||||
th.appendChild(indicator);
|
||||
}
|
||||
|
||||
if (index === activeColumn) {
|
||||
indicator.textContent = order === 'asc' ? '▲' : '▼';
|
||||
indicator.style.visibility = 'visible';
|
||||
} else {
|
||||
indicator.textContent = '';
|
||||
indicator.style.visibility = 'hidden';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore table sort from session storage
|
||||
function restoreTableSort(tableId, numericColumns) {
|
||||
const sortKey = `${tableId}-sort`;
|
||||
const sortState = sessionStorage.getItem(sortKey);
|
||||
|
||||
if (sortState) {
|
||||
const {column, order} = JSON.parse(sortState);
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table.querySelector('tbody') || table;
|
||||
const rows = Array.from(tbody.querySelectorAll('tr.tr')).filter(row => !row.classList.contains('table-titles'));
|
||||
|
||||
if (rows.length > 0) {
|
||||
const isNumeric = numericColumns.includes(column);
|
||||
|
||||
rows.sort((a, b) => {
|
||||
let aVal, bVal;
|
||||
|
||||
if (tableId === 'conn-stats-table' && column === 3) {
|
||||
aVal = parseFloat(a.children[column].getAttribute('data-seconds')) || 0;
|
||||
bVal = parseFloat(b.children[column].getAttribute('data-seconds')) || 0;
|
||||
} else {
|
||||
aVal = a.children[column].textContent.trim();
|
||||
bVal = b.children[column].textContent.trim();
|
||||
|
||||
if (isNumeric) {
|
||||
aVal = parseFloat(aVal) || 0;
|
||||
bVal = parseFloat(bVal) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
||||
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
row.className = row.className.replace(/cbi-rowstyle-[12]/, (index % 2 === 0) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1');
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
updateSortIndicators(tableId, column, order);
|
||||
}
|
||||
}
|
||||
|
||||
// Bind click events to table headers
|
||||
function bindTableEvents(tableId, numericColumns) {
|
||||
const table = document.getElementById(tableId);
|
||||
if (!table) return;
|
||||
|
||||
const headers = table.querySelectorAll('th[data-sortable-row="true"]');
|
||||
headers.forEach((th, index) => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.style.userSelect = 'none';
|
||||
|
||||
const newTh = th.cloneNode(true);
|
||||
th.parentNode.replaceChild(newTh, th);
|
||||
|
||||
if (!newTh.querySelector('.sort-indicator')) {
|
||||
const indicator = document.createElement('span');
|
||||
indicator.className = 'sort-indicator';
|
||||
indicator.style.fontSize = '0.8em';
|
||||
indicator.style.marginLeft = '4px';
|
||||
indicator.style.display = 'inline-block';
|
||||
indicator.style.width = '12px';
|
||||
indicator.style.textAlign = 'center';
|
||||
indicator.style.visibility = 'hidden';
|
||||
indicator.textContent = '';
|
||||
newTh.appendChild(indicator);
|
||||
}
|
||||
|
||||
newTh.addEventListener('click', () => {
|
||||
const isNumeric = numericColumns.includes(index);
|
||||
sortTable(tableId, index, isNumeric);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize table sorting functionality
|
||||
function initTableSort() {
|
||||
const tables = [
|
||||
{id: 'rewrite-stats-table', numericColumns: [1]},
|
||||
{id: 'pass-stats-table', numericColumns: [1]},
|
||||
{id: 'conn-stats-table', numericColumns: [3]}
|
||||
];
|
||||
|
||||
formatAllDurations();
|
||||
|
||||
tables.forEach(tableInfo => {
|
||||
bindTableEvents(tableInfo.id, tableInfo.numericColumns);
|
||||
restoreTableSort(tableInfo.id, tableInfo.numericColumns);
|
||||
});
|
||||
}
|
||||
|
||||
// Update statistics via AJAX
|
||||
async function updateStats() {
|
||||
try {
|
||||
const response = await fetch(window.location.href, {cache: "no-store"});
|
||||
const text = await response.text();
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(text, "text/html");
|
||||
const newRewriteTable = doc.querySelector("#rewrite-stats-table");
|
||||
const newPassTable = doc.querySelector("#pass-stats-table");
|
||||
const newConnTable = doc.querySelector("#conn-stats-table");
|
||||
const newLog = doc.querySelector("#cbid\\.ua3f\\.main\\.log");
|
||||
|
||||
if (newRewriteTable) {
|
||||
document.querySelector("#rewrite-stats-table").innerHTML = newRewriteTable.innerHTML;
|
||||
bindTableEvents('rewrite-stats-table', [1]);
|
||||
restoreTableSort('rewrite-stats-table', [1]);
|
||||
}
|
||||
if (newPassTable) {
|
||||
document.querySelector("#pass-stats-table").innerHTML = newPassTable.innerHTML;
|
||||
bindTableEvents('pass-stats-table', [1]);
|
||||
restoreTableSort('pass-stats-table', [1]);
|
||||
}
|
||||
if (newConnTable) {
|
||||
document.querySelector("#conn-stats-table").innerHTML = newConnTable.innerHTML;
|
||||
formatAllDurations();
|
||||
bindTableEvents('conn-stats-table', [3]);
|
||||
restoreTableSort('conn-stats-table', [3]);
|
||||
}
|
||||
if (newLog) {
|
||||
document.querySelector("#cbid\\.ua3f\\.main\\.log").value = newLog.value;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("update stats error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTableSort);
|
||||
} else {
|
||||
initTableSort();
|
||||
}
|
||||
|
||||
// Auto-refresh every 5 seconds
|
||||
setInterval(updateStats, 5000);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Sortable table headers */
|
||||
th[data-sortable-row="true"] {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
th[data-sortable-row="true"]:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
color: #0066cc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Rewrite Statistics Table */
|
||||
#rewrite-stats-table th:nth-child(1),
|
||||
#rewrite-stats-table td:nth-child(1) {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(2),
|
||||
#rewrite-stats-table td:nth-child(2) {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(3),
|
||||
#rewrite-stats-table td:nth-child(3) {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
#rewrite-stats-table th:nth-child(4),
|
||||
#rewrite-stats-table td:nth-child(4) {
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
/* Pass Statistics Table */
|
||||
#pass-stats-table th:nth-child(1),
|
||||
#pass-stats-table td:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(2),
|
||||
#pass-stats-table td:nth-child(2) {
|
||||
width: 10%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(3),
|
||||
#pass-stats-table td:nth-child(3) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#pass-stats-table th:nth-child(4),
|
||||
#pass-stats-table td:nth-child(4) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
/* Connection Statistics Table */
|
||||
#conn-stats-table th:nth-child(1),
|
||||
#conn-stats-table td:nth-child(1) {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(2),
|
||||
#conn-stats-table td:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(3),
|
||||
#conn-stats-table td:nth-child(3) {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
#conn-stats-table th:nth-child(4),
|
||||
#conn-stats-table td:nth-child(4) {
|
||||
width: 15%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@ -44,7 +44,7 @@ msgstr "User-Agent"
|
||||
msgid "User-Agent after rewrite"
|
||||
msgstr "重写后的 User-Agent"
|
||||
|
||||
msgid "User-Agent Regex Pattern"
|
||||
msgid "User-Agent Regex"
|
||||
msgstr "User-Agent 正则表达式"
|
||||
|
||||
msgid "Regular expression pattern for matching User-Agent"
|
||||
@ -53,7 +53,7 @@ msgstr "用于匹配 User-Agent,只有匹配成功的 User-Agent 才会被修
|
||||
msgid "Partial Replace"
|
||||
msgstr "部分替换"
|
||||
|
||||
msgid "Replace only the matched part of the User-Agent, only works when User-Agent Regex Pattern is not empty"
|
||||
msgid "Replace only the matched part of the User-Agent, only works when User-Agent Regex is not empty"
|
||||
msgstr "仅替换 User-Agent 正则匹配的部分,仅在 User-Agent 正则表达式非空时有效"
|
||||
|
||||
msgid "Direct Forward"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user