mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 09:03:56 +08:00
🌴 Sync 2026-10-30 02:13:56
This commit is contained in:
1 parent
8d20d896c0
commit
57d68d2cec
34 files changed
+1383
-304
No files matched your search
+2
-2
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=isp-split
|
||||
PKG_VERSION:=0.3.0
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=0.3.2
|
||||
PKG_RELEASE:=5
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=jihebube <jihebube@users.noreply.github.com>
|
||||
|
||||
@@ -7,6 +7,11 @@ STOP=10
|
||||
|
||||
start() {
|
||||
(
|
||||
# A-14:体检/状态缓存属主为 root,目录 700、文件 600。开机路径
|
||||
# 只做幂等收紧,不主动创建敏感文件。
|
||||
umask 077
|
||||
mkdir -p /etc/isp-split/state 2>/dev/null || true
|
||||
chmod 700 /etc/isp-split/state 2>/dev/null || true
|
||||
i=0
|
||||
while [ "$i" -lt 60 ]; do
|
||||
if nft list table inet mwan3 >/dev/null 2>&1; then
|
||||
|
||||
@@ -187,9 +187,9 @@ LINE_CHECK_ENABLE=1
|
||||
# 归属地隐私开关:1=允许向第三方查询出口 IP 归属地;0=只关闭归属地查询
|
||||
#
|
||||
# 开启后,体检会把**每条 WAN 的出口公网 IP**发送给第三方(默认百度,
|
||||
# 失败时 ip-api.com)查询归属地。只发送出口 IP,不发送内网地址、设备名、
|
||||
# MAC、流量内容。ip-api 使用 HTTP,传输不加密;介意时请设为 0,或把
|
||||
# LINE_CHECK_GEO_PROVIDER 固定为 baidu。
|
||||
# 默认只走百度 HTTPS)查询归属地。只发送出口 IP,不发送内网地址、设备名、
|
||||
# MAC、流量内容。ip-api 是显式选项且使用 HTTP 明文,不是默认兜底;介意时
|
||||
# 请设为 0,或把 LINE_CHECK_GEO_PROVIDER 固定为 baidu。
|
||||
# 关闭后不发起任何归属地请求,卡片显示「归属地:未查询(隐私开关关闭)」。
|
||||
# 省/市不可知,但仍可用本地 @isp_* 集合判断运营商大类,不需要外发。
|
||||
# 注意:此开关只关归属地;出口 IP 回显、STUN 与延迟探测仍会外发。
|
||||
@@ -197,7 +197,7 @@ LINE_CHECK_ENABLE=1
|
||||
LINE_CHECK_GEO=1
|
||||
|
||||
# 归属地服务商:auto | baidu | ip-api | off
|
||||
# auto 先百度(HTTPS、国内可达),失败转 ip-api(HTTP)
|
||||
# auto 只走百度(HTTPS、国内可达);ip-api 不再自动兜底
|
||||
# baidu 只用百度;失败显示「归属地未知」,不会自动转 HTTP 源
|
||||
# ip-api 只用 ip-api(HTTP,明文)
|
||||
# off 等同 LINE_CHECK_GEO=0
|
||||
@@ -244,7 +244,10 @@ stun.cloudflare.com:3478
|
||||
"
|
||||
|
||||
# 出口 IP 回显服务(逗号分隔,按顺序尝试)。只用于拿公网出口 IP。
|
||||
LINE_CHECK_ECHO_URLS="https://api.ipify.org,https://ipv4.icanhazip.com"
|
||||
# F-23:默认回显源改为国内可达的 HTTPS 服务,按顺序尝试。允许返回裸 IP
|
||||
# 或含 IP 的文本(如 ipip.net 的“当前 IP:x.x.x.x”),取第一个合法 IPv4。
|
||||
# 不要放境外 api.ipify.org / icanhazip.com:国内运营商网络经常 RST/超时。
|
||||
LINE_CHECK_ECHO_URLS="https://ip.3322.net,https://myip.ipip.net,https://ddns.oray.com/checkip"
|
||||
|
||||
# 体检请求的 User-Agent。部分运营商站点对默认 curl UA 返回 412/418,
|
||||
# 必须带浏览器 UA;不要改成空值。
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
/etc/init.d/isp-split enable >/dev/null 2>&1
|
||||
|
||||
# A-14:state 首次创建就 700;旧版本留下的 755 也在这里纠正。
|
||||
mkdir -p /etc/isp-split/state 2>/dev/null || true
|
||||
chmod 700 /etc/isp-split/state 2>/dev/null || true
|
||||
|
||||
# 启用每日更新(只改数据,不重写 mwan3 配置)
|
||||
if [ -x /etc/init.d/cron ]; then
|
||||
mkdir -p /etc/crontabs
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
# sh teardown.sh --list # 打印清单(selftest 与 postrm 漂移断言用),不动作
|
||||
# 选项:
|
||||
# --keep-conf-bak # 保留 /etc/isp-split.conf.bak.*(默认删除并逐条打印)
|
||||
# --i-know-what-im-doing # 显式确认非默认 BASE(默认只允许 /etc/isp-split
|
||||
# # 或 ISP_SPLIT_DESTDIR 下的沙箱路径)。系统目录
|
||||
# # (/、/etc、/usr、/var、/bin、/sbin、/lib…)永远拒绝
|
||||
#
|
||||
# 为什么默认删 .conf.bak.*:它们是安装脚本每次覆盖安装都会新生成的副本,
|
||||
# 不是用户手写的备份。留着等于"卸载"留下一批与工具同名同前缀的孤儿文件,
|
||||
@@ -54,6 +57,12 @@ case "$DESTDIR" in
|
||||
esac
|
||||
|
||||
BASE=${ISP_SPLIT_BASE:-$DESTDIR/etc/isp-split}
|
||||
# 归一化尾部斜杠(含多个):`/etc//` 与 `/etc` 必须落到同一条拒绝分支;
|
||||
# 根路径保持 `/`,否则空串会绕过下面的空值检查。
|
||||
if [ "$BASE" != "/" ]; then
|
||||
BASE=$(printf '%s' "$BASE" | sed 's#/*$##')
|
||||
[ -n "$BASE" ] || BASE=/
|
||||
fi
|
||||
CONF=${ISP_SPLIT_CONF:-$DESTDIR/etc/isp-split.conf}
|
||||
CRONTAB=${ISP_SPLIT_CRONTAB:-$DESTDIR/etc/crontabs/root}
|
||||
LIB=${ISP_SPLIT_LIB:-$DESTDIR/usr/lib/isp-split}
|
||||
@@ -69,9 +78,56 @@ CRON_SERVICE=${CRON_SERVICE:-$DESTDIR/etc/init.d/cron}
|
||||
RPCD_SERVICE=${RPCD_SERVICE:-$DESTDIR/etc/init.d/rpcd}
|
||||
|
||||
KEEP_CONF_BAK=0
|
||||
IWKD=0
|
||||
SCOPE=""
|
||||
|
||||
say() { printf '[teardown] %s\n' "$*"; }
|
||||
|
||||
# A-10:BASE 是 `rm -rf "$BASE"` 的唯一根,必须在任何删除之前过白名单。
|
||||
# 真实事故:ISP_SPLIT_BASE=/etc 时,旧 cmd_files 直接 rm -rf /etc,
|
||||
# 而固定清单还会按 DESTDIR 删真实安装文件(附录 C)。
|
||||
# 规则:
|
||||
# 1) 永远拒绝空、/、相对路径、含 ..、以及系统目录本身;
|
||||
# 2) 真实根(DESTDIR 为空)只接受默认 /etc/isp-split,其它路径必须显式
|
||||
# --i-know-what-im-doing;
|
||||
# 3) 沙箱(DESTDIR 非空)只接受 DESTDIR 子树(防止演练误删宿主 /etc)。
|
||||
validate_base() {
|
||||
case "$BASE" in
|
||||
"" | "/" | "." | ".." | *..*)
|
||||
echo "ISP_SPLIT_BASE 非法: '$BASE'(拒绝空、/、相对路径与 ..)" >&2
|
||||
return 1
|
||||
;;
|
||||
/*) ;;
|
||||
*)
|
||||
echo "ISP_SPLIT_BASE 必须是绝对路径: '$BASE'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
case "$BASE" in
|
||||
/etc | /usr | /var | /bin | /sbin | /lib | /lib64 | /boot | /dev | /proc | /sys | /www | /root | /home | /tmp | /mnt | /opt)
|
||||
echo "ISP_SPLIT_BASE 指向系统目录,拒绝执行: '$BASE'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 沙箱:BASE 必须在 DESTDIR 里面;否则演练会删到宿主路径。
|
||||
if [ -n "$DESTDIR" ]; then
|
||||
case "$BASE" in
|
||||
"$DESTDIR"/*) return 0 ;;
|
||||
*)
|
||||
echo "ISP_SPLIT_BASE 必须在 ISP_SPLIT_DESTDIR ($DESTDIR) 子树内: '$BASE'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# 真实根:默认路径直接放行;其它路径必须显式确认。
|
||||
[ "$BASE" = "/etc/isp-split" ] && return 0
|
||||
[ "$IWKD" = "1" ] && return 0
|
||||
echo "ISP_SPLIT_BASE 不是默认的 /etc/isp-split: '$BASE'" >&2
|
||||
echo " 如确需操作该目录,请加 --i-know-what-im-doing;系统目录永远不会被接受。" >&2
|
||||
return 1
|
||||
}
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# 只有真实根(DESTDIR 为空)才动服务:沙箱演练里重启服务既没意义,也会误伤宿主。
|
||||
@@ -86,13 +142,13 @@ svc_restart() {
|
||||
|
||||
# 文件/目录清单(相对 DESTDIR)。--list 与 files 段共用,杜绝两处漂移。
|
||||
files_list() {
|
||||
cat <<'EOF'
|
||||
cat <<EOF
|
||||
path usr/sbin/isp-split
|
||||
path etc/init.d/isp-split
|
||||
dir usr/lib/isp-split
|
||||
path etc/isp-split.conf
|
||||
glob etc/isp-split.conf.bak.*
|
||||
dir etc/isp-split
|
||||
base $BASE
|
||||
path etc/uci-defaults/99-isp-split
|
||||
dir www/luci-static/resources/view/isp-split
|
||||
dir www/luci-static/resources/isp-split
|
||||
@@ -139,6 +195,7 @@ print_list() {
|
||||
case "$_k" in
|
||||
path) echo " $_v" ;;
|
||||
dir) echo " $_v/(目录)" ;;
|
||||
base) echo " $_v/(数据目录,随 ISP_SPLIT_BASE 推导)" ;;
|
||||
glob) echo " $_v(历史备份,默认删除;--keep-conf-bak 保留)" ;;
|
||||
esac
|
||||
done
|
||||
@@ -229,6 +286,31 @@ _state_read() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# A-05:DNSDIR 白名单(与 isp-split 主脚本同一口径)。只允许 $BASE 子树
|
||||
# 或 /tmp/dnsmasq*.d 形态;/etc、/、.. 一律不参与删除。
|
||||
_dnsdir_ok() {
|
||||
_ddo=${1:-}
|
||||
case "$_ddo" in "" | "/" | "." | ".." | *..*) return 1 ;; esac
|
||||
case "$_ddo" in /*) ;; *) return 1 ;; esac
|
||||
while [ "$_ddo" != "/" ] && [ "${_ddo%/}" != "$_ddo" ]; do
|
||||
_ddo=${_ddo%/}
|
||||
done
|
||||
case "$_ddo" in
|
||||
"$BASE"/*) return 0 ;;
|
||||
/tmp/dnsmasq*.d | /tmp/*/dnsmasq*.d) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
# 删除片段的根目录限制:绝不 rm -f /isp-split.conf 或 /etc/isp-split.conf。
|
||||
_frag_delete_ok() {
|
||||
_fdo=${1:-}
|
||||
case "$_fdo" in "" | "/" | "/etc" | "." | ".." | *..*) return 1 ;; esac
|
||||
case "$_fdo" in /*) ;; *) return 1 ;; esac
|
||||
[ -d "$_fdo" ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# 片段可能残留的目录:运行配置里的 conf-dir=(权威源)+ UCI 当前值 +
|
||||
# 状态文件记录值 + 默认目录 + 旧版目录,去重后逐行输出。
|
||||
_frag_dirs() {
|
||||
@@ -238,7 +320,8 @@ _frag_dirs() {
|
||||
sed -n 's/^[[:space:]]*conf-dir=//p' "$_fd_f" 2>/dev/null
|
||||
done
|
||||
printf '%s\n' "${_frag_uci:-}" "${_frag_state:-}" \
|
||||
"$DNSMASQ_DEFAULT_DIR" "$DNSDIR"
|
||||
"$DNSMASQ_DEFAULT_DIR"
|
||||
_dnsdir_ok "$DNSDIR" && printf '%s\n' "$DNSDIR"
|
||||
} | tr ' ' '\n' |
|
||||
sed -e 's/,.*$//' -e 's/^"//' -e 's/"$//' -e 's:/*$::' |
|
||||
awk 'NF && !seen[$0]++'
|
||||
@@ -272,6 +355,7 @@ cmd_runtime() {
|
||||
# 2) 清片段。探测来源优先,但不依赖探测是否成功——目录列表是并集。
|
||||
_n=0
|
||||
for _d in $(_frag_dirs); do
|
||||
_frag_delete_ok "$_d" || continue
|
||||
[ -f "$_d/isp-split.conf" ] || continue
|
||||
rm -f "$_d/isp-split.conf" 2>/dev/null && _n=$((_n + 1))
|
||||
done
|
||||
@@ -313,6 +397,19 @@ cmd_files() {
|
||||
_err=0
|
||||
while read -r _k _p; do
|
||||
case "$_k" in
|
||||
base)
|
||||
# A-10:数据目录不再写死 etc/isp-split,而是跟随 BASE。
|
||||
# 默认路径与自定义路径走同一条删除分支,避免“清单删默认、
|
||||
# BASE 再删一次”的双路径。
|
||||
[ -n "$BASE" ] || continue
|
||||
[ -e "$BASE" ] || [ -L "$BASE" ] || continue
|
||||
if rm -rf "$BASE"; then
|
||||
_del=$((_del + 1))
|
||||
else
|
||||
say "错误:删不掉 $BASE"
|
||||
_err=1
|
||||
fi
|
||||
;;
|
||||
path)
|
||||
[ -e "$DESTDIR/$_p" ] || [ -L "$DESTDIR/$_p" ] || continue
|
||||
if rm -f "$DESTDIR/$_p"; then
|
||||
@@ -358,17 +455,9 @@ cmd_files() {
|
||||
$(files_list)
|
||||
EOF
|
||||
|
||||
# BASE 可被 ISP_SPLIT_BASE 改名(自检沙箱),清单里的是默认路径,这里补上
|
||||
# 真实值。ISP_SPLIT_LIB 故意不参与删除:它只用于离线自检,可能指到源码树,
|
||||
# 删它会把仓库自己的 usr/lib/isp-split 抹掉。
|
||||
if [ "$BASE" != "$DESTDIR/etc/isp-split" ] && [ -d "$BASE" ]; then
|
||||
if rm -rf "$BASE"; then
|
||||
_del=$((_del + 1))
|
||||
else
|
||||
say "错误:删不掉 $BASE"
|
||||
_err=1
|
||||
fi
|
||||
fi
|
||||
# ISP_SPLIT_LIB 故意不参与删除:它只用于离线自检,可能指到源码树,
|
||||
# 删它会把仓库自己的 usr/lib/isp-split 抹掉。数据目录已在上面的
|
||||
# base 分支随 BASE 推导删除(A-10)。
|
||||
|
||||
if [ "$_del" -gt 0 ]; then
|
||||
say "已删除 $_del 项文件/目录"
|
||||
@@ -387,6 +476,7 @@ EOF
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--keep-conf-bak) KEEP_CONF_BAK=1 ;;
|
||||
--i-know-what-im-doing) IWKD=1 ;;
|
||||
--list) SCOPE=list ;;
|
||||
all | mwan3 | runtime | files) SCOPE=$1 ;;
|
||||
-h | --help)
|
||||
@@ -402,10 +492,13 @@ while [ $# -gt 0 ]; do
|
||||
done
|
||||
|
||||
if [ -z "$SCOPE" ]; then
|
||||
echo "用法: sh teardown.sh all|mwan3|runtime|files|--list [--keep-conf-bak]" >&2
|
||||
echo "用法: sh teardown.sh all|mwan3|runtime|files|--list [--keep-conf-bak] [--i-know-what-im-doing]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# A-10:任何实际动作(含 --list 之外的删除)之前先验证 BASE。
|
||||
validate_base || exit 1
|
||||
|
||||
case "$SCOPE" in
|
||||
list) print_list ;;
|
||||
mwan3) cmd_mwan3 ;;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-honk
|
||||
PKG_VERSION:=1.1.4
|
||||
PKG_RELEASE:=29
|
||||
PKG_RELEASE:=30
|
||||
|
||||
LUCI_TITLE:=LuCI support for honk
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
@@ -334,7 +334,8 @@ function ensureEditorStyles() {
|
||||
'.cm-format-btn { margin: 0 !important; cursor: pointer !important; }',
|
||||
'.cbi-value:has(.CodeMirror) { align-items: flex-start !important; }',
|
||||
'.cbi-value:has(.CodeMirror) > .cbi-value-title { padding-top: 5px !important; flex-grow: 0 !important; }',
|
||||
'.cbi-value:has(.CodeMirror) .cbi-value-field { flex: 1 1 0% !important; min-width: 0 !important; width: auto !important; }',
|
||||
// 26rem basis: when the row is narrower, the editor wraps under its title instead of being squeezed beside it
|
||||
'.cbi-value:has(.CodeMirror) .cbi-value-field { flex: 1 1 26rem !important; min-width: 0 !important; width: auto !important; }',
|
||||
'.CodeMirror {',
|
||||
' border: 1px solid var(--border, var(--hairline, var(--border-color-medium, #ccc))) !important;',
|
||||
' border-radius: var(--radius-base, 4px);',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-isp-split
|
||||
PKG_VERSION:=0.3.0
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=0.3.2
|
||||
PKG_RELEASE:=5
|
||||
PKG_LICENSE:=MIT
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
@@ -49,7 +49,7 @@ define Package/luci-app-isp-split/install
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/isp-split/isp-split.css \
|
||||
$(1)/www/luci-static/resources/isp-split/isp-split.css
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/isp-split/logos
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/isp-split/logos/*.svg \
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/isp-split/logos/*.png \
|
||||
$(1)/www/luci-static/resources/isp-split/logos/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/isp-split/overview.js \
|
||||
$(1)/www/luci-static/resources/view/isp-split/overview.js
|
||||
|
||||
@@ -254,25 +254,74 @@
|
||||
background: var(--isp-accent, var(--isp-accent-other));
|
||||
}
|
||||
|
||||
/* 卡头(方案 B,0.3.1):44px 图标左置 + 名称右排。
|
||||
0.3.0 是「名称左、20px 图标右」,运营商多起来时右角的小图标很难认,
|
||||
且各家 SVG 的视觉重心不一致;改成左置大图后,卡片读起来像一张名片:
|
||||
先认标志,再看名字,最后看数字。 */
|
||||
.isp-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.isp-op-name {
|
||||
font-size: 0.98rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--isp-text);
|
||||
letter-spacing: 0.02em;
|
||||
/* 四列布局下内容宽 ~218px:44px 图标 + 10px 间距后,
|
||||
长运营商名(ISP_TABLE 自定义标签)不要顶破卡片 */
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 图标位:logo 与「无 logo 的 glyph 占位」共用 44×44 的盒子,
|
||||
两者视觉同构——卡片并排时卡头高度、名称基线完全对齐。 */
|
||||
.isp-op-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
color: var(--isp-accent, var(--isp-accent-other));
|
||||
opacity: 0.9;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
/* 品牌 PNG(192×192 透明底):按盒子缩放,不拉伸、不裁切 */
|
||||
.isp-brand-logo {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 深色主题的 logo 提亮(实测,不是"看着差不多"):
|
||||
电信官方蓝 #004080 的 luminance 太低,直接贴到深色卡面 (#222) 上对比度只有
|
||||
1.55:1——44px 下像蒙了一层灰。只提亮不改色相(brightness/saturate):
|
||||
电信 1.55:1 → 3.31:1 联通 2.47:1 → 3.84:1 移动 2.46:1 → 5.31:1
|
||||
(前一列按 PNG 原色对 #222 算,后一列从深色截图里采 logo 像素实测。)
|
||||
浅色主题不动:白底上三家分别是 10.27 / 6.44 / 6.48:1,提亮反而失真。
|
||||
LuCI 的 bootstrap 主题用 :root[data-darkmode="true"] 切整套变量,这里跟随
|
||||
同一个属性——用户点顶栏开关或跟系统深色,都会命中。 */
|
||||
[data-darkmode="true"] .isp-brand-logo {
|
||||
filter: brightness(1.55) saturate(1.05);
|
||||
}
|
||||
|
||||
/* 兜底卡:与运营商卡同构(顶部色条 + 44px 卡头 + 大数字)。
|
||||
它是「未归属任何运营商」的那一格,所以身份色用中性灰而不是某家的品牌色。 */
|
||||
.isp-card-fb {
|
||||
--isp-accent: var(--isp-accent-other);
|
||||
}
|
||||
|
||||
/* 网格里只有一条「没有启用的运营商」提示时,让它横跨整行,
|
||||
否则会被 repeat(4) 挤成 1/4 宽的一条细缝 */
|
||||
.isp-cards > .isp-empty {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.isp-big {
|
||||
@@ -819,17 +868,30 @@
|
||||
|
||||
.lc-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 与状态页同一个卡头语言(0.3.1):44px logo/glyph 左置。
|
||||
映射不到运营商的 wan(isp 为空、自定义源)用线稿 glyph 占位,盒子同尺寸。 */
|
||||
.lc-head-ico {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 44px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.lc-title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
/* 加了 44px 图标后,标题占中间的剩余宽度,评分仍然贴右 */
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.lc-name {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="715" viewBox="-29.771 0 5703 1554" width="2500"><g fill="#02489d"><path d="m1014.229 261s-28-31-65-31c0 0-81 0-136 175 0 0-47 153-47 380 0 0 0 214 44 365 0 0 44 153 108 153 0 0 45 0 83-84 0 0 38-84 54-221l120-30s-31 255-127 420c0 0-99 166-224 166 0 0-150 0-256-225 0 0-106-225-106-544 0 0 0-319 106-546 0 0 106-226 256-226 0 0 150 0 257 230z"/><g><path d="M1436.229 568s-207 171 -537 254c0 0 -331 82 -594 28c0 0 -263 -56 -302 -217c0 0 -33 -134 112 -282c0 0 143 -147 400 -245l17 66s-150 64 -233 145c0 0 -85 85 -66 158c0 0 25 93 201 121c0 0 175 27 402 -29c0 0 225 -56 369 -163c0 0 142 -108 118 -202c0 0 -20 -81 -164 -116l-22 -86s187 7 311 69c0 0 127 62 153 167c0 0 41 160 -165 332"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 743 B |
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" viewBox="0 0 192.756 192.756"><g fill-rule="evenodd" clip-rule="evenodd"><g><path d="M59.293 54.077c0 -20.53 16.605 -37.136 37.135 -37.136c20.429 0 37.136 16.605 37.136 37.136c0 20.429 -16.707 37.035 -37.136 37.035c-20.53 -0.001 -37.135 -16.606 -37.135 -37.035zM75.596 47.434l3.724 3.02l15.397 -12.982c1.409 -1.107 3.22 -1.711 5.132 -1.711s3.725 0.705 5.234 1.812l19.422 15.8v3.623l-17.209 -13.888c-1.309 -1.007 -2.816 -1.51 -4.529 -1.51c-1.709 0 -3.32 0.604 -4.629 1.61l-0.201 0.101l-0.1 0.101c-1.611 1.409 -9.561 8.051 -13.587 11.372h0.101c0.805 1.308 2.918 1.51 4.83 0.402h0.101c4.328 -3.724 8.252 -7.044 8.655 -7.447c1.209 -1.006 2.717 -1.711 4.328 -1.711c1.609 0 3.02 0.604 4.125 1.51L118.769 57.6l3.623 3.019l2.113 1.711l-0.402 0.101l0.402 0.402s-16.404 13.989 -20.529 17.511c-2.014 2.114 -4.932 3.422 -8.152 3.422c-2.919 0 -5.736 -1.107 -7.75 -3.019L67.646 64.14v-5.133l20.329 16.505c1.409 1.006 3.019 1.61 4.931 1.61s3.724 -0.705 5.133 -1.812c0.805 -0.705 13.082 -11.171 18.617 -15.8l-3.623 -3.019l-15.498 13.084c-1.509 1.006 -3.22 1.61 -5.132 1.61c-2.013 0 -3.824 -0.604 -5.334 -1.812l-19.423 -15.8V49.95l17.31 13.989c1.208 0.906 2.717 1.51 4.428 1.51c1.812 0 3.422 -0.604 4.63 -1.61l0.201 -0.201l0.101 -0.101c1.711 -1.409 9.56 -7.951 13.585 -11.373h-0.1v-0.101l-1.006 -0.705c-1.008 -0.503 -2.416 -0.302 -3.725 0.403c-0.1 0 -0.1 0.101 -0.1 0c-4.328 3.724 -8.354 7.044 -8.756 7.447c-1.107 1.107 -2.717 1.711 -4.327 1.711c-1.51 0 -2.919 -0.503 -4.126 -1.409L73.483 49.246l-0.101 0.101l-3.522 -2.918l-1.007 -1.107l-1.207 -1.006h0.201l-0.201 -0.201s16.404 -13.989 20.53 -17.511c2.113 -2.113 4.931 -3.422 8.151 -3.422c3.02 0 5.737 1.208 7.749 3.02l20.43 16.706v5.132l-20.328 -16.605c-1.41 -1.006 -3.02 -1.61 -4.83 -1.61a8.446 8.446 0 0 0 -5.234 1.811c-0.805 0.702 -12.983 11.169 -18.518 15.798z" fill="#2767b0"/></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="1367" viewBox="0.122 228.756 611.755 334.503"><path fill="#D91A21" d="M380.786 389.207l20.073-19.89-15.851-15.729 15.851-15.974-20.073-19.706-15.79 15.667-10.465-10.465c-16.646-16.708-44.003-16.708-60.71 0-8.446 8.384-12.607 19.461-12.607 30.478 0 10.894 4.101 21.909 12.607 30.355 16.707 16.769 44.063 16.769 60.71 0a1481.013 1481.013 0 0 0 10.465-10.466l15.79 15.73zm-46.022-25.337c-5.875 5.875-15.239 5.875-21.053 0-2.815-2.815-4.223-6.671-4.223-10.343a15.23 15.23 0 0 1 4.223-10.526c5.813-5.813 15.178-5.813 21.053 0 4.59 4.651 10.404 10.526 10.404 10.526s-5.814 5.631-10.404 10.343zm264.567-40.82c-16.708-16.708-44.003-16.708-60.771 0-4.713 4.835-10.465 10.465-10.465 10.465l-15.852-15.728-19.89 19.706 15.789 15.973-15.789 15.729 19.89 19.891 15.79-15.79s5.753 5.691 10.465 10.466c16.77 16.769 44.064 16.769 60.771 0 8.385-8.446 12.607-19.462 12.607-30.355s-4.16-21.973-12.545-30.357zm-19.951 40.82c-5.753 5.875-15.116 5.875-20.931 0-4.712-4.712-10.465-10.343-10.465-10.343s5.753-5.875 10.465-10.526c5.814-5.813 15.178-5.813 20.931 0 2.876 2.877 4.284 6.671 4.284 10.526 0 3.672-1.346 7.527-4.284 10.343zm-112.73-10.343s46.695-46.757 51.286-51.408c16.829-16.769 16.829-43.941 0-60.832-16.708-16.708-43.881-16.708-60.772 0-6.548 6.731-10.403 10.526-10.403 10.526s-3.795-3.795-10.404-10.526c-16.83-16.708-44.003-16.708-60.833 0-16.769 16.891-16.769 44.063 0 60.832l51.408 51.408s-46.757 46.573-51.408 51.286c-16.769 16.769-16.769 44.003 0 60.833 16.83 16.707 44.003 16.707 60.833 0 6.609-6.732 10.404-10.649 10.404-10.649s3.855 3.917 10.403 10.649c16.892 16.707 44.064 16.707 60.772 0 16.829-16.83 16.829-44.064 0-60.833-4.591-4.713-51.286-51.286-51.286-51.286zm-71.237-71.298c-5.692-5.875-5.692-15.116 0-20.931 5.813-5.691 15.238-5.691 20.93 0l10.526 10.526-15.851 15.729 19.829 19.951 15.851-15.851 15.851 15.851 19.829-19.951-15.79-15.729s4.896-4.957 10.527-10.526c5.752-5.691 15.116-5.691 20.869 0 5.813 5.814 5.813 15.056 0 20.931-6.06 6.059-51.47 51.592-51.47 51.592s-45.165-45.533-51.101-51.592zm102.694 163.466c-5.753 5.691-15.116 5.691-20.869 0-5.631-5.569-10.526-10.466-10.526-10.466l15.789-15.851-19.828-19.891-15.912 15.79-15.852-15.851-19.828 19.89 15.851 15.852s-4.957 4.896-10.526 10.465c-5.691 5.691-15.116 5.691-20.931 0-5.691-5.753-5.691-15.239 0-20.931 5.937-6.059 51.225-51.592 51.225-51.592s45.41 45.533 51.469 51.592c5.751 5.692 5.751 15.24-.062 20.993z"/></svg>
|
||||
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -33,9 +33,51 @@ var ICONS = {
|
||||
refresh: '<path d="M20 11a8 8 0 1 0-2.3 6.3"/><path d="M20 4v7h-7"/>',
|
||||
pulse: '<path d="M2 12h4l2.5-6 3.5 12 2.5-6H22"/>',
|
||||
chevron: '<path d="M6 9l6 6 6-6"/>',
|
||||
shield: '<path d="M12 3l7 3v6c0 4-3 7-7 9-4-2-7-5-7-9V6z"/>'
|
||||
shield: '<path d="M12 3l7 3v6c0 4-3 7-7 9-4-2-7-5-7-9V6z"/>',
|
||||
/* 运营商兜底 glyph:wan 卡映射不到品牌 logo 时用它们占位
|
||||
(与状态页同一套 24 格线稿;视图之间不共享模块,故此处自带一份) */
|
||||
tel: '<path d="M12 9v12"/><circle cx="12" cy="5" r="2"/><path d="M8.6 8.4a5 5 0 0 1 6.8 0"/><path d="M6 5.8a8.4 8.4 0 0 1 12 0"/>',
|
||||
uni: '<circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M8 6h8"/><path d="M7 8l4 8"/><path d="M17 8l-4 8"/>',
|
||||
cmcc: '<rect x="7" y="3" width="10" height="18" rx="2"/><path d="M11 18h2"/>',
|
||||
cernet: '<path d="M3 9.5 12 5l9 4.5-9 4.5z"/><path d="M7 11.5V16c0 1.2 2.2 2.2 5 2.2s5-1 5-2.2v-4.5"/>',
|
||||
other: '<circle cx="12" cy="12" r="7"/><path d="M12 5v14"/>'
|
||||
};
|
||||
|
||||
/* wan 卡的 isp 源码 → glyph(与状态页 GLYPH_FOR 同源) */
|
||||
var GLYPH_FOR = {
|
||||
chinatelecom: 'tel', chinanet: 'tel', telecom: 'tel',
|
||||
unicom_cnc: 'uni', unicom: 'uni',
|
||||
cmcc: 'cmcc', mobile: 'cmcc',
|
||||
cernet: 'cernet'
|
||||
};
|
||||
|
||||
/* 品牌 logo:与状态页用的是同一组 PNG(同一路径、同一文件名)。
|
||||
wan 卡按 linecheck JSON 里的 isp 字段归属映射;映射不到就走 GLYPH_FOR/占位。 */
|
||||
var BRAND_LOGO = {
|
||||
chinatelecom: 'logo-chinatelecom.png',
|
||||
chinanet: 'logo-chinatelecom.png',
|
||||
telecom: 'logo-chinatelecom.png',
|
||||
unicom_cnc: 'logo-unicom.png',
|
||||
unicom: 'logo-unicom.png',
|
||||
cmcc: 'logo-mobile.png',
|
||||
mobile: 'logo-mobile.png'
|
||||
};
|
||||
|
||||
var HEAD_ICO = 44;
|
||||
|
||||
function brandLogo(src, size) {
|
||||
var file = BRAND_LOGO[src];
|
||||
if (!file)
|
||||
return null;
|
||||
var img = document.createElement('img');
|
||||
img.src = L.resource('isp-split/logos/' + file);
|
||||
img.alt = src;
|
||||
img.width = size || HEAD_ICO;
|
||||
img.height = size || HEAD_ICO;
|
||||
img.className = 'isp-brand-logo';
|
||||
return img;
|
||||
}
|
||||
|
||||
function svgIcon(name, size) {
|
||||
var span = document.createElement('span');
|
||||
span.className = 'isp-ico';
|
||||
@@ -68,8 +110,12 @@ function fmtAge(sec) {
|
||||
}
|
||||
|
||||
function fmtMs(v) {
|
||||
if (v === null || v === undefined || v === '' || v === 0)
|
||||
/* F-24b:null = 未测/失败,显示“—”;"<1" 与历史 0 都显示 "<1ms"。
|
||||
0 不再作为“没测到”的哨兵(亚毫秒 DNS 曾经被四舍五入成 0)。 */
|
||||
if (v === null || v === undefined || v === '')
|
||||
return '—';
|
||||
if (v === 0 || v === '<1')
|
||||
return '<1ms';
|
||||
return v + 'ms';
|
||||
}
|
||||
|
||||
@@ -427,6 +473,14 @@ return view.extend({
|
||||
var lat = w.latency || {};
|
||||
var geo = w.geo || {};
|
||||
var http = w.http || {};
|
||||
/* F-23:exit_probe 是 shell 侧逐 URL 记录的探测明细。有明细但
|
||||
exit_ip 为空 => 回显源不可达/返回格式不符;连明细都没有 =>
|
||||
没有发起探测(curl 缺失/未配置),文案保持“可能未真正出网”。
|
||||
所有 JSON 字段只作为 E() 的文本子项,绝不拼进 HTML 解析路径。 */
|
||||
var ep = Array.isArray(w.exit_probe) ? w.exit_probe : [];
|
||||
var exitText = w.exit_ip || (ep.length
|
||||
? _('回显源不可达(建议更换 LINE_CHECK_ECHO_URLS)')
|
||||
: _('未取到(可能未真正出网)'));
|
||||
var detail = E('div', { 'class': 'lc-detail', 'hidden': '' });
|
||||
|
||||
var toggle = E('button', {
|
||||
@@ -457,6 +511,12 @@ return view.extend({
|
||||
sc.grade === 'C' ? 'var(--isp-warn)' : 'var(--isp-bad)')
|
||||
}, [
|
||||
E('div', { 'class': 'lc-head' }, [
|
||||
/* D2:与状态页同一套卡头——44px 左置。
|
||||
wan 卡按 isp 归属取品牌 logo;自定义 ISP / 未识别(isp 为空)
|
||||
走 GLYPH_FOR 的线稿 glyph,同一 44px 盒子,卡头高度不变。 */
|
||||
E('span', { 'class': 'lc-head-ico' }, [
|
||||
brandLogo(w.isp) || svgIcon(GLYPH_FOR[w.isp] || 'pulse', HEAD_ICO)
|
||||
]),
|
||||
E('div', { 'class': 'lc-title' }, [
|
||||
E('span', { 'class': 'lc-name' }, [ w.iface || '?' ]),
|
||||
E('span', { 'class': 'lc-dev' }, [ w.device || '' ]),
|
||||
@@ -477,7 +537,7 @@ return view.extend({
|
||||
E('span', { 'class': 'lc-tag is-muted' }, [ classText(w.ip_class) ])
|
||||
]),
|
||||
row(_('出口 IP'), [
|
||||
E('span', {}, [ w.exit_ip || _('未取到(可能未真正出网)') ]),
|
||||
E('span', {}, [ exitText ]),
|
||||
multi
|
||||
]),
|
||||
row(_('运营商'), [ w.isp_label || _('未知') ]),
|
||||
|
||||
@@ -35,7 +35,13 @@ var ICONS = {
|
||||
uni: '<circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M8 6h8"/><path d="M7 8l4 8"/><path d="M17 8l-4 8"/>',
|
||||
cmcc: '<rect x="7" y="3" width="10" height="18" rx="2"/><path d="M11 18h2"/>',
|
||||
cernet: '<path d="M3 9.5 12 5l9 4.5-9 4.5z"/><path d="M7 11.5V16c0 1.2 2.2 2.2 5 2.2s5-1 5-2.2v-4.5"/>',
|
||||
other: '<circle cx="12" cy="12" r="7"/><path d="M12 5v14"/>'
|
||||
other: '<circle cx="12" cy="12" r="7"/><path d="M12 5v14"/>',
|
||||
/* 兜底流量卡的身份 glyph:地球 + 经纬线("未归属任何运营商"的那部分流量)。
|
||||
与上面一族同为 24 格 stroke 线稿(stroke-width 1.8、round 收口),
|
||||
44px 下线条不会糊;颜色走 currentColor,深浅主题都不用另做一套。 */
|
||||
net: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/>' +
|
||||
'<path d="M12 3.5c2.4 2.4 3.8 5.4 3.8 8.5s-1.4 6.1-3.8 8.5"/>' +
|
||||
'<path d="M12 3.5C9.6 5.9 8.2 8.9 8.2 12s1.4 6.1 3.8 8.5"/>'
|
||||
};
|
||||
|
||||
var GLYPH_FOR = {
|
||||
@@ -45,17 +51,22 @@ var GLYPH_FOR = {
|
||||
cernet: 'cernet'
|
||||
};
|
||||
|
||||
/* 品牌 logo(运营商官方标志,fill 型多色 SVG,静态文件按请求加载) */
|
||||
/* 品牌 logo(运营商官方风格标志,192×192 透明底 PNG,静态文件按请求加载)。
|
||||
0.3.1 起换图不换名:文件名仍是 logo-<运营商>,只把扩展名从 .svg 改成 .png。
|
||||
PNG 是位图,深浅两个主题共用一份即可(透明底天然跟随背景)。 */
|
||||
var BRAND_LOGO = {
|
||||
chinatelecom: 'logo-chinatelecom.svg',
|
||||
chinanet: 'logo-chinatelecom.svg',
|
||||
telecom: 'logo-chinatelecom.svg',
|
||||
unicom_cnc: 'logo-unicom.svg',
|
||||
unicom: 'logo-unicom.svg',
|
||||
cmcc: 'logo-mobile.svg',
|
||||
mobile: 'logo-mobile.svg'
|
||||
chinatelecom: 'logo-chinatelecom.png',
|
||||
chinanet: 'logo-chinatelecom.png',
|
||||
telecom: 'logo-chinatelecom.png',
|
||||
unicom_cnc: 'logo-unicom.png',
|
||||
unicom: 'logo-unicom.png',
|
||||
cmcc: 'logo-mobile.png',
|
||||
mobile: 'logo-mobile.png'
|
||||
};
|
||||
|
||||
/* 卡头图标统一 44px:logo 与「无 logo 的 glyph 占位」同尺寸,视觉同构 */
|
||||
var HEAD_ICO = 44;
|
||||
|
||||
function brandLogo(src, size) {
|
||||
var file = BRAND_LOGO[src];
|
||||
if (!file)
|
||||
@@ -63,8 +74,8 @@ function brandLogo(src, size) {
|
||||
var img = document.createElement('img');
|
||||
img.src = L.resource('isp-split/logos/' + file);
|
||||
img.alt = src;
|
||||
img.width = size || 20;
|
||||
img.height = size || 20;
|
||||
img.width = size || HEAD_ICO;
|
||||
img.height = size || HEAD_ICO;
|
||||
img.className = 'isp-brand-logo';
|
||||
return img;
|
||||
}
|
||||
@@ -360,6 +371,14 @@ return view.extend({
|
||||
return false;
|
||||
if (/^\s*#/.test(String(r.set || '')) || /^\s*#/.test(String(r.src || '')))
|
||||
return false;
|
||||
/* D1-3:教育网(cernet)退出状态页,纯 UI 退场。
|
||||
后端数据行照旧保留(status --json / 诊断 / 导出都不受影响),
|
||||
只是这一版不把它算进运营商网格:cernet 用户量小,指标常年为空,
|
||||
混在电信/联通/移动里只会稀释「一眼看三家」的信息密度。
|
||||
set 名与 src 一起判:ISP_TABLE 里 cernet 的集合名可以改,
|
||||
src 改不了。 */
|
||||
if (r.src === 'cernet' || r.set === 'isp_cernet')
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -369,7 +388,6 @@ return view.extend({
|
||||
? _('读取状态失败:') + this.statusError
|
||||
: _('没有启用的运营商。检查 /etc/isp-split.conf 里的 ISP_TABLE。')
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
var maxPackets = 0;
|
||||
@@ -401,8 +419,13 @@ return view.extend({
|
||||
'style': '--isp-accent: ' + accent
|
||||
}, [
|
||||
E('div', { 'class': 'isp-card-head' }, [
|
||||
E('span', { 'class': 'isp-op-name' }, [ r.label || r.src ]),
|
||||
E('span', { 'class': 'isp-op-ico' }, [ brandLogo(r.src, 20) || svgIcon(GLYPH_FOR[r.src] || 'other', 20) ])
|
||||
/* 方案 B:logo 44px 左置 + 运营商名。
|
||||
没有品牌 logo 的来源(自定义源 / other)走同一尺寸的 glyph 占位,
|
||||
两张卡并排时卡头高度与基线一致,不会一张高一卡矮。 */
|
||||
E('span', { 'class': 'isp-op-ico' }, [
|
||||
brandLogo(r.src) || svgIcon(GLYPH_FOR[r.src] || 'other', HEAD_ICO)
|
||||
]),
|
||||
E('span', { 'class': 'isp-op-name' }, [ r.label || r.src ])
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('span', { 'class': 'isp-big' }, [ r.exists ? fmtNum(packets) : '—' ]),
|
||||
@@ -430,6 +453,22 @@ return view.extend({
|
||||
}, [ '—' ])
|
||||
]));
|
||||
});
|
||||
|
||||
/* D1-4:兜底流量并入同一张运营商网格,成为「第 4 张卡」。
|
||||
桌面四列(.isp-cards 是 repeat(4)):三家运营商 + 兜底正好一行四格;
|
||||
ISP_TABLE 多开运营商时按四列继续换行,兜底始终排在最后,不会被挤到
|
||||
另一行只剩一半宽。卡头与运营商卡同构:44px glyph + 名称。
|
||||
内容由 paintFallback() 写进 #isp-fallback(paintOps 之后调用)。 */
|
||||
box.appendChild(E('article', {
|
||||
'class': 'isp-card isp-card-op isp-card-fb',
|
||||
'style': '--isp-accent: var(--isp-accent-other)'
|
||||
}, [
|
||||
E('div', { 'class': 'isp-card-head' }, [
|
||||
E('span', { 'class': 'isp-op-ico' }, [ svgIcon('net', HEAD_ICO) ]),
|
||||
E('span', { 'class': 'isp-op-name' }, [ _('兜底流量') ])
|
||||
]),
|
||||
E('div', { 'id': 'isp-fallback' })
|
||||
]));
|
||||
},
|
||||
|
||||
/* ---------------- 双线健康 ---------------- */
|
||||
@@ -485,7 +524,11 @@ return view.extend({
|
||||
box.appendChild(list);
|
||||
},
|
||||
|
||||
/* ---------------- 兜底流量 ---------------- */
|
||||
/* ---------------- 兜底流量(第 4 张运营商卡的主体) ----------------
|
||||
口径与 0.3.0 完全一致:未命中任何运营商集合的连接数(首包)与流量。
|
||||
卡头在 paintOps() 里画(44px glyph + 名称),这里只负责主体,
|
||||
字号/间距与运营商卡的 isp-big / isp-op-sub 同一套类名,因此
|
||||
两张卡的数字基线和单位位置自然对齐。 */
|
||||
|
||||
paintFallback: function() {
|
||||
var box = document.getElementById('isp-fallback');
|
||||
@@ -714,18 +757,15 @@ return view.extend({
|
||||
E('div', { 'id': 'isp-load-error', 'class': 'isp-err', 'style': 'display:none' }),
|
||||
E('div', { 'id': 'isp-cards', 'class': 'isp-cards' }),
|
||||
|
||||
E('div', { 'class': 'isp-grid-2' }, [
|
||||
/* 兜底流量不再是独立大卡:它已经并入顶部运营商网格的第 4 格
|
||||
(见 paintOps)。这里腾出来的位置让双线健康 / 数据源健康度 /
|
||||
数据时间排成一行三格(isp-grid-3,断点 1100→2 列、820→1 列),
|
||||
页面从「2+2+2」变成「4 张运营商 + 3 张明细」,视线更顺。 */
|
||||
E('div', { 'class': 'isp-grid-3' }, [
|
||||
E('section', { 'class': 'isp-card' }, [
|
||||
E('h3', {}, [ _('双线健康') ]),
|
||||
E('div', { 'id': 'isp-lines' })
|
||||
]),
|
||||
E('section', { 'class': 'isp-card' }, [
|
||||
E('h3', {}, [ _('兜底流量') ]),
|
||||
E('div', { 'id': 'isp-fallback' })
|
||||
])
|
||||
]),
|
||||
|
||||
E('div', { 'class': 'isp-grid-2' }, [
|
||||
E('section', { 'class': 'isp-card' }, [
|
||||
E('h3', {}, [ _('数据源健康度') ]),
|
||||
E('div', { 'id': 'isp-sources' })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"luci-app-isp-split": {
|
||||
"description": "Grant access to ISP split status and control",
|
||||
"description": "Grant access to ISP split status and control. WARNING: write = execute ANY isp-split subcommand as root (wire/apply/conf set/uninstall); grant only to fully trusted admins. read = conf + whole state tree (public IP/geo/STUN).",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"file": [ "read", "stat" ]
|
||||
|
||||
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openvpn-plus
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Manfred Jaider <info@masmbit.com>
|
||||
|
||||
|
||||
@@ -49,6 +49,18 @@ Even though the traffic has to pass through **Double-NAT** and simple port forwa
|
||||
- **Simple Profile Import & Export:** Upload existing `.ovpn` profiles or multi-key configuration files to start new client connections instantly.
|
||||
- **Clear Status Table:** Shows a small overview of running tunnels, active process IDs (PIDs), encryption types, and live data transfer.
|
||||
|
||||
## Screenshots
|
||||
|
||||
| 01 - Server/Client View | 02 - Status View |
|
||||
| :---: | :---: |
|
||||
| <img width="1200" alt="01" src="https://github.com/user-attachments/assets/b5f13132-0414-4490-9c80-9a361e1bda40" /> | <img width="1200" alt="02" src="https://github.com/user-attachments/assets/7635f2bd-4839-45e4-9804-d01ce38bde7b" />
|
||||
| <img width="2398" alt="03" src="https://github.com/user-attachments/assets/0170376e-3dfe-4ca3-8f03-d450cc7ed0e4" /> | <img width="2430" alt="15" src="https://github.com/user-attachments/assets/1710419c-4f14-4594-a3a4-d8147ec1fe50" />
|
||||
| <img width="1796" alt="09" src="https://github.com/user-attachments/assets/1c77aefe-197d-4e64-ab87-898ff50faab1" /> | <img width="1798" alt="10" src="https://github.com/user-attachments/assets/f37365a6-4090-443b-89fe-05a5cd92e714" />
|
||||
| <img width="1798" alt="13" src="https://github.com/user-attachments/assets/147250df-079c-4f77-9c5c-00819b4b495c" /> | <img width="1800" alt="11" src="https://github.com/user-attachments/assets/814bbeb0-a0b8-4b38-93b8-03a978d75692" />
|
||||
| <img width="1798" alt="12" src="https://github.com/user-attachments/assets/57726ec2-c18d-4531-96ec-0d58a818648a" /> | <img width="2384" alt="04" src="https://github.com/user-attachments/assets/8470fa8c-b4a4-4369-b9c5-bfbd8e27526d" />
|
||||
| <img width="2382" alt="05" src="https://github.com/user-attachments/assets/1ef91c41-675c-40d8-a9da-644370ac2fdf" /> | <img width="2394" alt="06" src="https://github.com/user-attachments/assets/95fbdf13-142f-4f67-9117-15eef266e304" />
|
||||
|
||||
|
||||
## Configuration Layout
|
||||
|
||||
The application manages settings inside `/etc/config/openvpn`:
|
||||
|
||||
@@ -46,6 +46,10 @@ msgstr "Client-Instanz hinzufügen"
|
||||
msgid "Add Office"
|
||||
msgstr "Büro hinzufügen"
|
||||
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn-keygen.js:94
|
||||
msgid "Add OpenVPN Server Instance"
|
||||
msgstr "OpenVPN-Server-Instanz hinzufügen"
|
||||
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn.js:64
|
||||
msgid "Add Server Instance"
|
||||
msgstr "Server-Instanz hinzufügen"
|
||||
@@ -1527,4 +1531,3 @@ msgstr "ist aktuell"
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn.js:136
|
||||
msgid "packets"
|
||||
msgstr "Pakete"
|
||||
|
||||
@@ -46,6 +46,10 @@ msgstr ""
|
||||
msgid "Add Office"
|
||||
msgstr ""
|
||||
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn-keygen.js:94
|
||||
msgid "Add OpenVPN Server Instance"
|
||||
msgstr ""
|
||||
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn.js:64
|
||||
msgid "Add Server Instance"
|
||||
msgstr ""
|
||||
|
||||
@@ -46,6 +46,10 @@ msgstr "添加客户端实例"
|
||||
msgid "Add Office"
|
||||
msgstr "添加办公室"
|
||||
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn-keygen.js:94
|
||||
msgid "Add OpenVPN Server Instance"
|
||||
msgstr "添加 OpenVPN 服务器实例"
|
||||
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn.js:64
|
||||
msgid "Add Server Instance"
|
||||
msgstr "添加服务器实例"
|
||||
@@ -1529,4 +1533,3 @@ msgstr "当前正在"
|
||||
#: root/www/luci-static/resources/view/vpn/openvpn.js:136
|
||||
msgid "packets"
|
||||
msgstr "数据包"
|
||||
|
||||
@@ -14,7 +14,7 @@ if [ "$ACTION" != "ifup" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# STRIP LOOPBACK: Ignore the internal system loopback to prevent a premature trigger
|
||||
# Ignore the internal system loopback to prevent a premature trigger
|
||||
if [ "$INTERFACE" = "loopback" ]; then
|
||||
exit 0
|
||||
fi
|
||||
@@ -23,51 +23,65 @@ LOCK_FILE="/var/run/luci-app-openvpn-plus-bootup.lock"
|
||||
|
||||
# Mutex implementation preventing parallel execution threads from racing each other
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
# Clean verification via signal 0: check if the holding PID is still alive
|
||||
if kill -0 $(cat "$LOCK_FILE" 2>/dev/null) 2>/dev/null; then
|
||||
# check if the holding PID is still alive
|
||||
lock_pid=$(cat "$LOCK_FILE" 2>/dev/null)
|
||||
if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Register active PID into the lock file to serialize the bootup checks
|
||||
echo "$$" > "$LOCK_FILE"
|
||||
printf "%s\n" "$$" > "$LOCK_FILE"
|
||||
|
||||
# Set traps to delete the lock file reliably on unexpected script exits
|
||||
trap "rm -f $LOCK_FILE" EXIT INT TERM
|
||||
trap 'rm -f "$LOCK_FILE"' EXIT INT TERM
|
||||
|
||||
logger -t openvpn-luci "Bootup: Interface '${INTERFACE}' is up. Checking active OpenVPN profile running state in 10 seconds..."
|
||||
|
||||
# Give the system firewall and routing tables enough time to init
|
||||
sleep 10
|
||||
|
||||
# Loop through all configured openvpn sections inside the UCI configuration
|
||||
uci -q show openvpn | grep "=openvpn" | while read -r line; do
|
||||
# Extract the pure instance identity string from the current UCI line
|
||||
inst="${line##*.}"
|
||||
inst="${inst%%=*}"
|
||||
openvpn_sections=$(uci -q show openvpn | awk -F'[.=]' '/=openvpn$/ {print $2}')
|
||||
|
||||
# Process the instance only if it is explicitly enabled by the user
|
||||
if [ "$(uci -q get openvpn.${inst}.enabled)" = "1" ]; then
|
||||
# Bulletproof process check utilizing the immune syslog tag pattern
|
||||
if ! ps | grep -v "grep" | grep -q "openvpn(${inst})"; then
|
||||
logger -t openvpn-luci "Bootup: Detected unstarted instance '${inst}'. Initializing process."
|
||||
# Exit if no OpenVPN sections exist
|
||||
if [ -z "$openvpn_sections" ]; then
|
||||
rm -f "$LOCK_FILE"
|
||||
trap - EXIT INT TERM
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Restart the service cleanly to clear out any dead zombie states or locked ports
|
||||
/etc/init.d/openvpn stop "$inst" >/dev/null 2>&1
|
||||
sleep 1 >/dev/null 2>&1
|
||||
/etc/init.d/openvpn restart "$inst" >/dev/null 2>&1
|
||||
sleep 1 >/dev/null 2>&1
|
||||
/etc/init.d/openvpn reload "$inst" >/dev/null 2>&1
|
||||
else
|
||||
logger -t openvpn-luci "Bootup: Instance '${inst}' is already running safely."
|
||||
fi
|
||||
fi
|
||||
# Get the current process list
|
||||
proc_dump=$(ps)
|
||||
|
||||
# Loop through all configured openvpn sections
|
||||
for inst in $openvpn_sections; do
|
||||
[ -z "$inst" ] && continue
|
||||
|
||||
# Process the instance only if it is explicitly enabled by the user
|
||||
if [ "$(uci -q get openvpn."$inst".enabled)" = "1" ]; then
|
||||
case "$proc_dump" in
|
||||
*"openvpn(${inst})"* )
|
||||
logger -t openvpn-luci "Bootup: Instance '${inst}' is already running safely."
|
||||
;;
|
||||
*)
|
||||
logger -t openvpn-luci "Bootup: Detected unstarted instance '${inst}'. Initializing process."
|
||||
|
||||
# Restart the service cleanly to clear out any dead zombie states or locked ports
|
||||
/etc/init.d/openvpn stop "$inst" >/dev/null 2>&1
|
||||
sleep 1
|
||||
/etc/init.d/openvpn restart "$inst" >/dev/null 2>&1
|
||||
sleep 1
|
||||
/etc/init.d/openvpn reload "$inst" >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
# Delete the lock file manually after successful execution
|
||||
# Delete the lock file and reset all traps to system default values before exiting
|
||||
rm -f "$LOCK_FILE"
|
||||
|
||||
# Unregister and reset all traps to system default values before exiting
|
||||
trap - EXIT INT TERM
|
||||
|
||||
exit 0
|
||||
|
||||
# Debug: Manual hotplug simulation (e.g. wan up):
|
||||
# ACTION="ifup" INTERFACE="wan" ash -x /etc/hotplug.d/iface/35-luci-app-openvpn-plus-bootup
|
||||
+33
-21
@@ -6,7 +6,7 @@
|
||||
# This is free software, licensed under the Apache License, Version 2.0.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
# luci-app-openvpn-plus : OpenWrt hotplug lifecycle manager for ddns server and client common name
|
||||
# luci-app-openvpn-plus : hotplug lifecycle manager for ddns server and client common name
|
||||
# /etc/hotplug.d/openvpn/35-luci-app-openvpn-plus-lifecycle
|
||||
|
||||
# Ensure we catch the active OpenVPN name context across all OpenWrt variants
|
||||
@@ -25,12 +25,12 @@ PID_FILE="/var/run/openvpn-ddns-${ovpn_instance}.pid"
|
||||
case "$ACTION" in
|
||||
|
||||
up)
|
||||
|
||||
inst_role=$(uci -q get openvpn."$ovpn_instance".role)
|
||||
|
||||
# Save the active interface name into the OpenWrt UCI configuration
|
||||
if [ "$inst_role" = "server" ] || [ "$inst_role" = "client" ]; then
|
||||
if [ -n "$ovpn_dev" ]; then
|
||||
# Only write and commit if the devname actually changed
|
||||
current_devname=$(uci -q get openvpn."$ovpn_instance".devname)
|
||||
if [ "$current_devname" != "$ovpn_dev" ]; then
|
||||
uci -q set openvpn."$ovpn_instance".devname="$ovpn_dev"
|
||||
@@ -60,40 +60,52 @@ case "$ACTION" in
|
||||
start-stop-daemon -S -b -m -p "$PID_FILE" -x /usr/libexec/luci-app-openvpn-plus -- ddnsservice "$domain" "$url" "$ovpn_instance"
|
||||
fi
|
||||
fi
|
||||
|
||||
# CLIENT: EXTRACT COMMON NAME
|
||||
elif [ "$inst_role" = "client" ]; then
|
||||
cn_update="0"
|
||||
# Extract the last connection initiation line from logread for this specific instance
|
||||
log_line=$(logread | grep "openvpn(${ovpn_instance})" | grep "Peer Connection Initiated" | tail -n 1)
|
||||
|
||||
if [ -n "$log_line" ]; then
|
||||
# Parse the pure CN string from inside the brackets (Example: [OfficeServer] -> OfficeServer)
|
||||
server_cn=$(echo "$log_line" | sed -n 's/.*: \[\([^]]*\)\].*Peer Connection Initiated.*/\1/p')
|
||||
# Let logread stream only the lines belonging to this specific instance.
|
||||
log_line=$(logread -e "openvpn(${ovpn_instance})" | grep "Peer Connection Initiated" | tail -n 1)
|
||||
|
||||
if [ -n "$server_cn" ]; then
|
||||
# Write the resolved Common Name safely into the OpenWrt UCI configuration
|
||||
uci -q set openvpn."$ovpn_instance".server_common_name="$server_cn"
|
||||
uci commit openvpn
|
||||
if [ -n "$log_line" ]; then
|
||||
# Parse the pure CN string from inside the brackets (Example: [OfficeServer] -> OfficeServer)
|
||||
server_cn=$(echo "$log_line" | sed -n 's/.*\[\([^]]*\)\].*Peer Connection Initiated.*/\1/p')
|
||||
|
||||
if [ -n "$server_cn" ]; then
|
||||
# Only write and commit if the Common Name actually changed
|
||||
current_cn=$(uci -q get openvpn."$ovpn_instance".server_common_name)
|
||||
if [ "$current_cn" != "$server_cn" ]; then
|
||||
uci -q set openvpn."$ovpn_instance".server_common_name="$server_cn"
|
||||
uci commit openvpn
|
||||
logger -t openvpn-luci "Hotplug (${ovpn_instance}): set server_common_name ${server_cn}"
|
||||
fi
|
||||
cn_update="1"
|
||||
fi
|
||||
fi
|
||||
if [ "$cn_update" = "1" ]; then
|
||||
logger -t openvpn-luci "Hotplug (${ovpn_instance}): Server Common Name '${server_cn}' resolved"
|
||||
else
|
||||
logger -t openvpn-luci "Hotplug (${ovpn_instance}): Server Common Name not found in Peer Connection log entry"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$cn_update" = "1" ]; then
|
||||
logger -t openvpn-luci "Hotplug (${ovpn_instance}): Server Common Name '${server_cn}' resolved"
|
||||
else
|
||||
logger -t openvpn-luci "Hotplug (${ovpn_instance}): Server Common Name not found in Peer Connection log entry"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
|
||||
route-pre-down|down)
|
||||
|
||||
# Clean application stop via standard system manager signals
|
||||
if start-stop-daemon -K -q -p "$PID_FILE" -t 2>/dev/null; then
|
||||
logger -t openvpn-luci "Hotplug: Stopping DDNS service for ${ovpn_instance} because tunnel is down."
|
||||
start-stop-daemon -K -q -p "$PID_FILE" 2>/dev/null
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
|
||||
# Debug: Manual hotplug simulation (e.g. instance1 up):
|
||||
# ACTION="up" INSTANCE="instance1" dev="tun0" ash -x /etc/hotplug.d/openvpn/35-luci-app-openvpn-plus-lifecycle
|
||||
|
||||
# Debug: Manual hotplug simulation (e.g. instance1 down):
|
||||
# ACTION="down" INSTANCE="instance1" dev="tun0" ash -x /etc/hotplug.d/openvpn/35-luci-app-openvpn-plus-lifecycle
|
||||
@@ -196,7 +196,7 @@ internal_checkpath() {
|
||||
# Characters are safely escaped with a backslash to protect the shell syntax.
|
||||
return 1
|
||||
;;
|
||||
*$'\t'*|$'\n'*|$'\r'*)
|
||||
*$'\t'*|*$'\n'*|*$'\r'*)
|
||||
# Block control characters like tabs, newlines, and carriage returns natively
|
||||
return 1
|
||||
;;
|
||||
@@ -738,36 +738,36 @@ symlink() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
local ovpn_instance="$1"
|
||||
local action="$2"
|
||||
local ovpn_instance="${1}"
|
||||
local action="${2}"
|
||||
local raw_display_name="${3:-}"
|
||||
|
||||
# Check if instance name is valid
|
||||
if ! internal_checkname "$ovpn_instance"; then
|
||||
if ! internal_checkname "${ovpn_instance}"; then
|
||||
printf "%s\n" "ERROR: Invalid character format in instance name." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local export_static_path="/etc/openvpn/luci/${ovpn_instance}.ovpn"
|
||||
local lan_ip=""
|
||||
local link_filename=""
|
||||
local download_filename=""
|
||||
local ram_link_dir="/tmp/luci-ovpn-plus"
|
||||
local www_link_dir="/www/ovpn-downloads"
|
||||
|
||||
# Check if display name is valid
|
||||
if [ -n "$raw_display_name" ]; then
|
||||
if ! internal_checkname "$raw_display_name"; then
|
||||
if [ -n "${raw_display_name}" ]; then
|
||||
if ! internal_checkname "${raw_display_name}"; then
|
||||
printf "%s\n" "ERROR: Invalid character format in display name." >&2
|
||||
return 1
|
||||
fi
|
||||
link_filename="$raw_display_name"
|
||||
download_filename="${raw_display_name}"
|
||||
else
|
||||
link_filename="$ovpn_instance"
|
||||
download_filename="${ovpn_instance}"
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
case "${action}" in
|
||||
create)
|
||||
if [ ! -f "$export_static_path" ]; then
|
||||
if [ ! -f "${export_static_path}" ]; then
|
||||
printf "%s\n" "ERROR: Profile file missing." >&2
|
||||
return 1
|
||||
fi
|
||||
@@ -775,36 +775,47 @@ symlink() {
|
||||
# Create a 9-byte random secret token as hex safely
|
||||
local secret_token
|
||||
secret_token=$(dd if=/dev/urandom bs=9 count=1 2>/dev/null | hexdump -e '"%02x"')
|
||||
if [ -z "$secret_token" ]; then
|
||||
if [ -z "${secret_token}" ]; then
|
||||
printf "%s\n" "ERROR: Failed to generate secure token." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local session_dir="${ram_link_dir}/ovpn-tmp-${secret_token}-${link_filename}"
|
||||
mkdir -p "$session_dir"
|
||||
local session_dir="${ram_link_dir}/ovpn-tmp-${secret_token}-${ovpn_instance}"
|
||||
mkdir -p "${session_dir}"
|
||||
|
||||
# Define file path and copy profile with safe webserver permissions
|
||||
local token_protected_file="${session_dir}/${link_filename}.ovpn"
|
||||
cp -f "$export_static_path" "$token_protected_file" 2>/dev/null
|
||||
chmod 644 "$token_protected_file"
|
||||
local token_protected_file="${session_dir}/${download_filename}.ovpn"
|
||||
cp -f "${export_static_path}" "${token_protected_file}" 2>/dev/null
|
||||
chmod 644 "${token_protected_file}"
|
||||
|
||||
# Create webserver shortcut to the RAM folder securely
|
||||
if [ ! -L "$www_link_dir" ] && [ ! -d "$www_link_dir" ]; then
|
||||
ln -s "$ram_link_dir" "$www_link_dir"
|
||||
# Create directory with restricted access and block directory listing with index.html redirect
|
||||
if [ ! -d "${www_link_dir}" ]; then
|
||||
mkdir -p "${www_link_dir}"
|
||||
# Enforce ultra-strict permissions so only root/webserver can see the folder structure
|
||||
chmod 700 "${www_link_dir}"
|
||||
# create index.html with redirect users back to the server root (/)
|
||||
printf '%s\n' '<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0;URL=/" /></head><body></body></html>' > "${www_link_dir}/index.html"
|
||||
fi
|
||||
|
||||
if [ -f "$token_protected_file" ]; then
|
||||
local symlink_dir="${www_link_dir}/ovpn-tmp-${secret_token}-${ovpn_instance}"
|
||||
|
||||
# Link the specific session directory
|
||||
if [ ! -L "${symlink_dir}" ]; then
|
||||
ln -s "${session_dir}" "${symlink_dir}"
|
||||
fi
|
||||
|
||||
if [ -f "${token_protected_file}" ]; then
|
||||
# Get lan ip with internal lanip function
|
||||
lan_ip=$(lanip "lanip")
|
||||
[ -z "$lan_ip" ] && lan_ip="localhost"
|
||||
[ -z "${lan_ip}" ] && lan_ip="localhost"
|
||||
|
||||
# If the IP contains a colon (:), it is an IPv6 address and needs brackets []
|
||||
case "$lan_ip" in
|
||||
case "${lan_ip}" in
|
||||
*:*)
|
||||
lan_ip="[${lan_ip}]"
|
||||
;;
|
||||
esac
|
||||
printf "SUCCESS: http://%s/ovpn-downloads/ovpn-tmp-%s-%s/%s.ovpn\n" "$lan_ip" "$secret_token" "$link_filename" "$link_filename"
|
||||
printf "SUCCESS: http://%s/ovpn-downloads/ovpn-tmp-%s-%s/%s.ovpn\n" "${lan_ip}" "${secret_token}" "${ovpn_instance}" "${download_filename}"
|
||||
else
|
||||
printf "%s\n" "ERROR: Failed to create web export file." >&2
|
||||
return 1
|
||||
@@ -813,14 +824,35 @@ symlink() {
|
||||
|
||||
delete)
|
||||
# Safe deletion inside RAM directory only. Absolute path structure guaranteed.
|
||||
if [ -d "$ram_link_dir" ]; then
|
||||
rm -rf "${ram_link_dir}/ovpn-tmp-*-${link_filename}"
|
||||
if [ -d "${ram_link_dir}" ]; then
|
||||
# Find and remove the specific symlink from the webroot before cleaning up RAM
|
||||
local target_link
|
||||
for target_link in "${www_link_dir}"/ovpn-tmp-*-"${ovpn_instance}"; do
|
||||
[ -L "${target_link}" ] && rm -f "${target_link}"
|
||||
done
|
||||
|
||||
# Remove RAM base directory and the web symlink if it was the last file
|
||||
rmdir "$ram_link_dir" 2>/dev/null
|
||||
if [ ! -d "$ram_link_dir" ] && [ -L "$www_link_dir" ]; then
|
||||
rm -f "$www_link_dir"
|
||||
fi
|
||||
# Remove the temporary session files for this instance from RAM
|
||||
rm -rf "${ram_link_dir}"/ovpn-tmp-*-"${ovpn_instance}"
|
||||
|
||||
# Remove RAM base directory if it has become completely empty
|
||||
rmdir "${ram_link_dir}" 2>/dev/null
|
||||
|
||||
# Smart Web-Folder Cleanup: Check if files other than index.html exist
|
||||
if [ -d "${www_link_dir}" ]; then
|
||||
local has_profiles="0"
|
||||
local check_link
|
||||
# If a real symlink or folder matching the pattern exists, has_profiles becomes 1
|
||||
for check_link in "${www_link_dir}"/ovpn-tmp-*-*; do
|
||||
if [ -e "${check_link}" ] || [ -L "${check_link}" ]; then
|
||||
has_profiles="1"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Remove the web folder if no active profile links are left
|
||||
if [ "${has_profiles}" = "0" ]; then
|
||||
rm -rf "${www_link_dir}"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%s\n" "SUCCESS: Link directory and static profile deleted from RAM."
|
||||
else
|
||||
@@ -837,6 +869,7 @@ symlink() {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
# Helper to prevent routing loops for IPv4 and IPv6 subnets
|
||||
internal_checkloopback() {
|
||||
# internal (no shift)
|
||||
@@ -1878,69 +1911,108 @@ ucisortovpn() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Delete configuration and key files of deleted VPN instances
|
||||
# Universal function: deletes all files, folders or symlinks if the instance is not active
|
||||
internal_cleaninst() {
|
||||
# internal (no shift)
|
||||
local active_instances="${1}"
|
||||
local file_pattern="${2}"
|
||||
local f
|
||||
local fname
|
||||
local instname
|
||||
|
||||
# Loop through all files, folders or links that match the pattern
|
||||
for f in $file_pattern; do
|
||||
# Skip if it does not exist AND is not a symlink (handles dead links safely)
|
||||
if [ ! -e "${f}" ] && [ ! -L "${f}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# a) Get the pure filename or folder name without the path
|
||||
fname="${f##*/}"
|
||||
# b) Keep only the word "instance" and everything after it
|
||||
instname="instance${fname#*instance}"
|
||||
# c) Remove the extension starting from the first dot (.key, .nft, .status)
|
||||
instname="${instname%%.*}"
|
||||
|
||||
# Compare the extracted name with our active list in RAM using strict space boundaries
|
||||
case " ${active_instances} " in
|
||||
*" ${instname} "*)
|
||||
# Instance is active, keep the file
|
||||
;;
|
||||
*)
|
||||
# SECURITY GUARD: To avoid rm manipulation, check the name before the deletion
|
||||
if internal_checkname "${instname}"; then
|
||||
# Safely remove files, dead symlinks or directories
|
||||
rm -rf "${f}"
|
||||
logger -t openvpn-luci "Cleanup: Removed orphaned item '${f}'."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
# no argument (no shift)
|
||||
local conf_file
|
||||
local instname
|
||||
local active_list=""
|
||||
local conf_dir="/etc/openvpn/luci"
|
||||
local key_dir="/etc/openvpn/keys"
|
||||
local run_dir="/var/run"
|
||||
local ram_link_dir="/tmp/luci-ovpn-plus"
|
||||
local www_link_dir="/www/ovpn-downloads"
|
||||
|
||||
# Step 1: Stop here if there are no instance config files on the router
|
||||
ls /etc/openvpn/luci/instance*.conf >/dev/null 2>&1 || return 0
|
||||
|
||||
# Step 2: Loop through all existing instance configuration files
|
||||
for conf_file in "$conf_dir"/instance*.conf; do
|
||||
if [ -f "$conf_file" ]; then
|
||||
# Step 1: Build the list of ACTIVE instances from the instance*.conf files
|
||||
for conf_file in "${conf_dir}"/instance*.conf; do
|
||||
if [ -f "${conf_file}" ]; then
|
||||
instname="${conf_file##*/}"
|
||||
instname="${instname%.conf}"
|
||||
|
||||
# CRITICAL SECURITY GUARD: Validate the instance name before any file operation.
|
||||
# This completely neutralizes dangerous path traversal attacks via rm -rf!
|
||||
if ! internal_checkname "$instname"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Step 3: If the UCI section does not exist, delete files and crypto keys
|
||||
if ! uci -q get openvpn."$instname" >/dev/null; then
|
||||
rm -f "$conf_file"
|
||||
rm -f "$conf_dir/${instname}.nft"
|
||||
rm -f "$conf_dir/${instname}.jmp.nft"
|
||||
rm -f "$conf_dir/${instname}.s2s.nft"
|
||||
rm -f "$key_dir/ca_${instname}.crt"
|
||||
rm -f "$key_dir/dh_${instname}.pem"
|
||||
rm -f "$key_dir/server_${instname}.crt"
|
||||
rm -f "$key_dir/client_${instname}.crt"
|
||||
rm -f "$key_dir/server_${instname}.key"
|
||||
rm -f "$key_dir/client_${instname}.key"
|
||||
rm -f "$key_dir/server_${instname}.tls2.key"
|
||||
rm -f "$key_dir/client_${instname}.tls2.key"
|
||||
|
||||
# Delete the static .ovpn file for this specific deleted instance
|
||||
rm -f "$conf_dir/${instname}.ovpn"
|
||||
|
||||
# remove ddns status file for this instance
|
||||
rm -f "/var/run/openvpn-ddns-${instname}.status"
|
||||
|
||||
# Clean up the specific RAM files for this deleted instance
|
||||
if [ -d "$ram_link_dir" ]; then
|
||||
rm -rf "${ram_link_dir}"/ovpn-tmp-*-*"${instname}"
|
||||
fi
|
||||
if internal_checkname "${instname}"; then
|
||||
# Enforce space separation to prevent partial substring matches later
|
||||
active_list="${active_list} ${instname}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 4: Safely clean up the empty base directories and the web symlink
|
||||
if [ -d "$ram_link_dir" ]; then
|
||||
# Removes the RAM base folder ONLY if it is completely empty
|
||||
rmdir "$ram_link_dir" 2>/dev/null
|
||||
fi
|
||||
# Clean up the web shortcut if the RAM directory does not exist anymore
|
||||
if [ ! -d "$ram_link_dir" ] && [ -L "$www_link_dir" ]; then
|
||||
rm -f "$www_link_dir"
|
||||
fi
|
||||
# Step 2: Remove orphan instance files
|
||||
internal_cleaninst "${active_list}" "${conf_dir}/instance*.conf"
|
||||
internal_cleaninst "${active_list}" "${conf_dir}/instance*.nft"
|
||||
internal_cleaninst "${active_list}" "${conf_dir}/instance*.jmp.nft"
|
||||
internal_cleaninst "${active_list}" "${conf_dir}/instance*.s2s.nft"
|
||||
internal_cleaninst "${active_list}" "${conf_dir}/instance*.ovpn"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/ca_instance*.crt"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/dh_instance*.pem"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/server_instance*.crt"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/client_instance*.crt"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/server_instance*.key"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/client_instance*.key"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/server_instance*.tls2.key"
|
||||
internal_cleaninst "${active_list}" "${key_dir}/client_instance*.tls2.key"
|
||||
internal_cleaninst "${active_list}" "${run_dir}/openvpn-ddns-instance*.status"
|
||||
|
||||
# Step 3: Remove orphan symlink files
|
||||
internal_cleaninst "${active_list}" "${www_link_dir}/ovpn-tmp-*-*"
|
||||
internal_cleaninst "${active_list}" "${ram_link_dir}/ovpn-tmp-*-*"
|
||||
|
||||
# Remove the temporary RAM directory ONLY if it is completely empty
|
||||
[ -d "${ram_link_dir}" ] && rmdir "${ram_link_dir}" 2>/dev/null
|
||||
# Smart Web-Folder Cleanup: Check if files other than index.html exist
|
||||
if [ -d "${www_link_dir}" ]; then
|
||||
local has_profiles="0"
|
||||
local check_link
|
||||
# If a real symlink or folder matching the pattern exists, has_profiles becomes 1
|
||||
for check_link in "${www_link_dir}"/ovpn-tmp-*-*; do
|
||||
if [ -e "${check_link}" ] || [ -L "${check_link}" ]; then
|
||||
has_profiles="1"
|
||||
break
|
||||
fi
|
||||
done
|
||||
# Remove the web folder if no active profile links are left
|
||||
if [ "${has_profiles}" = "0" ]; then
|
||||
rm -rf "${www_link_dir}"
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ const TXT = {
|
||||
tls_key_server: _('Master Server Secret Key'),
|
||||
tls_key_client: _('Client Key'),
|
||||
title_main: _('OpenVPN Instance Key Generator'),
|
||||
add_server_instance: ('Add OpenVPN Server Instance'),
|
||||
add_server_instance: _('Add OpenVPN Server Instance'),
|
||||
unknown_asset: _('Unknown Asset'),
|
||||
unknown_structure: _('Unknown Structure'),
|
||||
use_unique_name_for_each_office: _('Important: Use a unique name for each office to configure the network routing correctly.'),
|
||||
@@ -231,8 +231,8 @@ const getKeyMeta = async function (keyfile, luci_app_openvpn_plus_callback) {
|
||||
if (!res || res.length === 0) {
|
||||
return result;
|
||||
}
|
||||
res = res.toUpperCase();
|
||||
if (res.startsWith('ERROR') || res.indexOf('ERROR') !== -1 || res.startsWith('INVALID')) {
|
||||
const resUpper = res.toUpperCase();
|
||||
if (resUpper.startsWith('ERROR') || resUpper.indexOf('ERROR') !== -1 || resUpper.startsWith('INVALID')) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ LUCI_TITLE:=LuCI Support for Automatic Partition Mount
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop
|
||||
PKG_VERSION:=2.0.5
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Sirpdboy <herboy2008@gmail.com>
|
||||
|
||||
@@ -30,6 +30,26 @@ logn(){
|
||||
echo -e " $*" | tee -a $LOG
|
||||
}
|
||||
|
||||
# 获取当前 overlay 的数据目录。
|
||||
# 不要写死 /overlay:部分固件(如 Kwrt/ImmortalWrt 的部分机型)把 rootfs_data
|
||||
# 挂在 /rom/overlay,而 /overlay 只存在于 preinit 的挂载命名空间里。
|
||||
get_overlay_src() {
|
||||
local src upper
|
||||
upper=$(sed -n 's/.*upperdir=\([^,]*\).*/\1/p' /proc/self/mountinfo 2>/dev/null | head -1)
|
||||
src="${upper%/upper}"
|
||||
if [ -n "$src" ] && [ -d "$src" ]; then
|
||||
echo "$src"
|
||||
return 0
|
||||
fi
|
||||
for src in /overlay /rom/overlay; do
|
||||
if [ -d "$src" ]; then
|
||||
echo "$src"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
load_uci_config() {
|
||||
if [ -f "/etc/config/$CONFIG" ]; then
|
||||
target_function=$(uci -q get $CONFIG.global.target_function)
|
||||
@@ -214,10 +234,10 @@ format_disk() {
|
||||
|
||||
# 获取系统盘
|
||||
get_system_disk() {
|
||||
rom_dev=$(df -h | grep boot | awk '{print $1}' | head -1 )
|
||||
[ -z ${rom_dev} ] && rom_dev=`df -P /boot 2>/dev/null | awk 'NR==2 {print $1}'`
|
||||
[ -z ${rom_dev} ] && rom_dev=`blkid -t TYPE="squashfs" |awk -F : '{print $1}'`
|
||||
[ -z ${rom_dev} ] && TDISK=`lsblk -l -o NAME,MOUNTPOINTS | grep -E '/rom'| grep -v '/rom/' | head -1 | awk '{print $1}'` && rom_dev="/dev/$TDISK"
|
||||
rom_dev=$(df -h 2>/dev/null | grep boot | awk '{print $1}' | head -1 )
|
||||
[ -z "${rom_dev}" ] && rom_dev=$(df -P /boot 2>/dev/null | awk 'NR==2 {print $1}')
|
||||
[ -z "${rom_dev}" ] && rom_dev=$(blkid -t TYPE="squashfs" 2>/dev/null | awk -F : 'NR==1 {print $1}')
|
||||
[ -z "${rom_dev}" ] && TDISK=`lsblk -l -o NAME,MOUNTPOINTS | grep -E '/rom'| grep -v '/rom/' | head -1 | awk '{print $1}'` && rom_dev="/dev/$TDISK"
|
||||
if [ -n "$rom_dev" ]; then
|
||||
case "$rom_dev" in
|
||||
/dev/sd[a-z][0-9]*)
|
||||
@@ -429,7 +449,10 @@ fdiskB() {
|
||||
block detect > /etc/config/fstab
|
||||
uci -q set fstab.@global[0].anon_mount='0'
|
||||
uci -q set fstab.@global[0].auto_mount='0'
|
||||
uci commit fstab
|
||||
# 注意:'uci commit fstab' 会让 procd 重载 /etc/init.d/fstab,而它的
|
||||
# stop() 是 '/sbin/block umount',会把 /rom 和当前 /overlay 一起卸载。
|
||||
# 扩展 overlay 时当前 overlay 正是配置拷贝源,所以此时不能提交。
|
||||
[ "$target_function" = "/overlay" ] || uci commit fstab
|
||||
|
||||
if [ "$target_function" = '/opt' ]; then
|
||||
/etc/init.d/dockerd stop >/dev/null 2> /dev/null
|
||||
@@ -483,6 +506,14 @@ fdiskB() {
|
||||
fi
|
||||
sleep 5
|
||||
if [[ "$target_function" = "/" || "$target_function" = "/overlay" ]]; then
|
||||
# 整盘被自动挂载(如 /dev/sda -> /mnt/sda)时 mkfs 会拒绝执行
|
||||
# ("apparently in use by the system"),所以先卸载整盘。
|
||||
if mount | grep -q "^/dev/$a "; then
|
||||
log "取消整盘挂载 /dev/$a ..."
|
||||
block umount /dev/$a >/dev/null 2> /dev/null
|
||||
umount /dev/$a >/dev/null 2> /dev/null
|
||||
sleep 1
|
||||
fi
|
||||
format_disk "/dev/$b" $format_type
|
||||
elif [[ "$format_type" != "0" || "$isfdisk" = "1" ]]; then
|
||||
format_disk "/dev/$b" $format_type
|
||||
@@ -523,7 +554,22 @@ fdiskB() {
|
||||
case "$target_function" in
|
||||
"/overlay")
|
||||
if [ "$keep_config" = "1" ]; then
|
||||
tar -C /overlay -cvf - . | tar -C /mnt/$b/ -xf - || tar -C /rom/overlay -cvf - . | tar -C /mnt/$b/ -xf -
|
||||
SRC=$(get_overlay_src)
|
||||
if [ -z "$SRC" ]; then
|
||||
log "错误:找不到当前 overlay 数据目录,已中止(未修改 fstab)"
|
||||
expquit 1
|
||||
fi
|
||||
log "正在从 $SRC 拷贝配置到 /mnt/$b ..."
|
||||
if ! tar -C "$SRC" -cf - . | tar -C /mnt/$b/ -xf -; then
|
||||
log "错误:配置拷贝失败,已中止(未修改 fstab,系统未受影响)"
|
||||
umount /dev/$b >/dev/null 2> /dev/null
|
||||
expquit 1
|
||||
fi
|
||||
if [ ! -f "/mnt/$b/upper/etc/config/network" ] && [ ! -f "/mnt/$b/etc/config/network" ]; then
|
||||
log "错误:拷贝校验失败(未找到 etc/config/network),已中止(未修改 fstab)"
|
||||
umount /dev/$b >/dev/null 2> /dev/null
|
||||
expquit 1
|
||||
fi
|
||||
umount /dev/$b >/dev/null 2> /dev/null
|
||||
[ $? -ne 0 ] && umount /mnt/$b >/dev/null 2> /dev/null
|
||||
[ $? -ne 0 ] && block umount /dev/$b >/dev/null 2> /dev/null
|
||||
@@ -781,9 +827,13 @@ stop() {
|
||||
|
||||
expquit() {
|
||||
rm -f $LOCK
|
||||
uci -q set fstab.@global[0].anon_mount='1'
|
||||
uci -q set fstab.@global[0].auto_mount='1'
|
||||
uci commit fstab
|
||||
# 只有非 overlay 模式才在开头改过 anon_mount/auto_mount,需要恢复;
|
||||
# overlay 模式为避免 block umount 没有提交,这里也就不能再 commit。
|
||||
if [ "$target_function" != "/overlay" ]; then
|
||||
uci -q set fstab.@global[0].anon_mount='1'
|
||||
uci -q set fstab.@global[0].auto_mount='1'
|
||||
uci commit fstab
|
||||
fi
|
||||
[ -e "/etc/config/dockerd" ] && /etc/init.d/dockerd restart >/dev/null 2> /dev/null
|
||||
[ "$1" = "2" ] && log "如果没生效,请重启设备"
|
||||
sleep 1
|
||||
|
||||
@@ -983,7 +983,7 @@ function renderLive(name, modem, graphs, board) {
|
||||
'%d / %d'.format(lc.tac, lc.global_cell_id) ]);
|
||||
var enb = fmt.fmtEnodeb(lc.global_cell_id);
|
||||
if (enb)
|
||||
srvRows.push([ term(_('eNodeB / Sector'), _('The Cell ID split into the base station (eNodeB ID = Cell ID / 256) and its cell, often one sector of the site (Cell ID % 256). Cells with the same eNodeB ID are on the same site')),
|
||||
srvRows.push([ term(_('eNB / Sector'), _('The Cell ID split into the base station (eNB = eNodeB, ID = Cell ID / 256) and its cell, often one sector of the site (Cell ID % 256). Cells with the same eNB ID belong to the same base station, which may drive radios at more than one site')),
|
||||
enb ]);
|
||||
}
|
||||
var nc = cells.nr5g_cell, sn = (cells.serving||{}).nr;
|
||||
|
||||
@@ -2852,9 +2852,10 @@ msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/wwand/status.js:986
|
||||
msgid ""
|
||||
"The Cell ID split into the base station (eNodeB ID = Cell ID / 256) and its "
|
||||
"cell, often one sector of the site (Cell ID % 256). Cells with the same "
|
||||
"eNodeB ID are on the same site"
|
||||
"The Cell ID split into the base station (eNB = eNodeB, ID = Cell ID / 256) "
|
||||
"and its cell, often one sector of the site (Cell ID % 256). Cells with the "
|
||||
"same eNB ID belong to the same base station, which may drive radios at more "
|
||||
"than one site"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/wwand/status.js:936
|
||||
@@ -3573,7 +3574,7 @@ msgid "disabled (SIM boots without PIN)"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/wwand/status.js:986
|
||||
msgid "eNodeB / Sector"
|
||||
msgid "eNB / Sector"
|
||||
msgstr ""
|
||||
|
||||
#: htdocs/luci-static/resources/view/wwand/sims.js:94
|
||||
|
||||
@@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-theme-vantage
|
||||
PKG_VERSION:=1.0.10
|
||||
PKG_RELEASE:=24
|
||||
PKG_RELEASE:=25
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
LUCI_TITLE:=Vantage theme
|
||||
|
||||
@@ -979,12 +979,11 @@ input, select, textarea, button { font: inherit; color: inherit; }
|
||||
line-height: 20px;
|
||||
transition: border-color .12s, box-shadow .12s;
|
||||
}
|
||||
.vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: calc(2 * var(--field-max)); min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; }
|
||||
.vt-main textarea, .cbi-input-textarea, .modal textarea { max-width: 100%; min-height: 88px; font-family: var(--mono); font-size: var(--fs-sm); resize: vertical; }
|
||||
/* read-only text (System and Kernel log) is a viewer, not a field: full width */
|
||||
.vt-main textarea[readonly] { max-width: none; }
|
||||
/* a short free-text option (System -> Notes: no rows set) is as wide as the
|
||||
fields beside it; editors that set their rows (Scheduled Tasks) stay wide */
|
||||
.cbi-value-field textarea.cbi-input-textarea:not([rows]) { max-width: var(--field-max); }
|
||||
/* System notes follow the neighbouring single-line fields. */
|
||||
body[data-page="admin-system-system"] .cbi-value[data-name="notes"] textarea { max-width: var(--field-max); }
|
||||
.vt-main select, .cbi-input-select, .modal select {
|
||||
appearance: none; -webkit-appearance: none;
|
||||
padding-right: 30px;
|
||||
@@ -1209,7 +1208,8 @@ input[type="button"], input[type="reset"], input[type="submit"]) {
|
||||
border-radius: var(--r);
|
||||
box-shadow: var(--shadow-pop);
|
||||
overflow-x: hidden; overflow-y: auto;
|
||||
transition: max-height .12s ease-out;
|
||||
/* LuCI focuses the selected item on transitionend; avoid animating its scroll range. */
|
||||
transition: max-height .001s;
|
||||
}
|
||||
.cbi-dropdown[open] > ul.dropdown[style*="bottom"] { top: auto; }
|
||||
.cbi-dropdown[open] > ul.dropdown > li {
|
||||
@@ -1952,6 +1952,8 @@ body.modal-overlay-active #modal_overlay { display: block; }
|
||||
font-size: var(--fs-lg);
|
||||
}
|
||||
#modal_overlay > .modal.cbi-modal { width: min(960px, 100%); }
|
||||
#modal_overlay > .modal:has(> textarea[readonly]) { width: min(1080px, 100%); }
|
||||
#modal_overlay > .modal > textarea[readonly] { max-width: 100%; }
|
||||
#modal_overlay > .modal .cbi-section { background: none; border: 0; padding: 0; margin: 0; box-shadow: none; }
|
||||
/* a modal's sections have no card padding, so nothing in them bleeds out
|
||||
by it: tables keep the modal's width (a bleed scrolled the section
|
||||
@@ -2067,7 +2069,7 @@ body.modal-overlay-active #modal_overlay { display: block; }
|
||||
.assoclist .td { vertical-align: middle; }
|
||||
/* free-standing tables (no section around them) become a card; clip, not
|
||||
hidden: a scroll container would capture the sticky header */
|
||||
#view > .table, #view > div > .table, .cbi-map > .table, #view > div > div > [data-tab-title]:not(.cbi-section) > .table {
|
||||
#view > .table, #view > div:not(.cbi-section) > .table, .cbi-map > .table, #view > div > div > [data-tab-title]:not(.cbi-section) > .table {
|
||||
margin: 0 0 var(--sp-4);
|
||||
background: var(--s1);
|
||||
border: 1px solid var(--line);
|
||||
@@ -2075,8 +2077,8 @@ body.modal-overlay-active #modal_overlay { display: block; }
|
||||
box-shadow: var(--v-shadow);
|
||||
overflow: clip;
|
||||
}
|
||||
:is(#view, #view > div, .cbi-map, [data-tab-title]:not(.cbi-section)) > .table .tr > :is(.th, .td):first-child { padding-left: var(--card-px); }
|
||||
:is(#view, #view > div, .cbi-map, [data-tab-title]:not(.cbi-section)) > .table .tr > :is(.th, .td):last-child { padding-right: var(--card-px); }
|
||||
:is(#view, #view > div:not(.cbi-section), .cbi-map, [data-tab-title]:not(.cbi-section)) > .table .tr > :is(.th, .td):first-child { padding-left: var(--card-px); }
|
||||
:is(#view, #view > div:not(.cbi-section), .cbi-map, [data-tab-title]:not(.cbi-section)) > .table .tr > :is(.th, .td):last-child { padding-right: var(--card-px); }
|
||||
#view > h3, #view > div > h3 { margin: var(--sp-6) 0 var(--sp-3); font-size: var(--fs-md); }
|
||||
/* LuCI 24.10: Status -> Overview's sections are #view's children with the
|
||||
heading first (25.12 wraps it in .cbi-title): it keeps the header row's
|
||||
@@ -2459,19 +2461,27 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
||||
.cbi-section-table-row:has(> .td[data-name="_stat"] big):first-child > .td { border-top: 0; }
|
||||
.cbi-section-table-row:not(:has(big)) > .td[data-name="_badge"] { padding-left: calc(var(--card-px) + 16px); }
|
||||
|
||||
/* progress bars: thin track, value text outside the fill */
|
||||
/* progress bars: thin track, value text above it and in flow, so larger
|
||||
browser fonts cannot run into the heading */
|
||||
.cbi-progressbar {
|
||||
height: 6px; margin: 20px 0 2px;
|
||||
background: var(--s3);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows: auto 6px;
|
||||
gap: 5px;
|
||||
height: auto; margin: 0 0 2px;
|
||||
background: none;
|
||||
border: 0; border-radius: 3px;
|
||||
overflow: visible;
|
||||
}
|
||||
.cbi-progressbar > div { border-radius: 3px; background: var(--accent-text); }
|
||||
.cbi-progressbar > div { grid-area: 2 / 1; height: 6px; z-index: 1; border-radius: 3px; background: var(--accent-text); }
|
||||
.cbi-progressbar::before {
|
||||
inset: auto 0 calc(100% + 5px) 0; padding: 0;
|
||||
position: static; inset: auto; padding: 0;
|
||||
display: block;
|
||||
color: var(--text-2);
|
||||
font: 500 var(--fs-micro)/1.2 var(--mono);
|
||||
white-space: normal; overflow-wrap: anywhere;
|
||||
}
|
||||
.cbi-progressbar::after { content: ""; grid-area: 2 / 1; height: 6px; border-radius: 3px; background: var(--s3); }
|
||||
|
||||
/* fused memory / storage meters (built by menu-vantage.js) */
|
||||
.v-src-hidden { display: none !important; }
|
||||
@@ -2517,15 +2527,23 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
||||
.v-meter-row > .v-meter-val b { color: var(--text); font-weight: 600; }
|
||||
.v-sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
|
||||
/* wide tables scroll inside their card, never the page. Sticky headers need
|
||||
a non-scrolling ancestor, so status tables only scroll when narrow. The
|
||||
card is as tall as its content, so it has nothing to scroll down to; a
|
||||
few pixels of overflow (Firefox on a scaled display) made it scroll */
|
||||
.cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { overflow-x: auto; overflow-y: hidden; }
|
||||
/* Share the widest table's intrinsic width with its headings and dividers. */
|
||||
@media (width > 720px) {
|
||||
.cbi-tblsection { display: grid; grid-template-columns: minmax(min-content, 1fr); }
|
||||
.cbi-section.cbi-tblsection > .table { width: auto !important; }
|
||||
.cbi-section:has(.cbi-tblsection) > .cbi-section-node { display: table; width: 100%; }
|
||||
.cbi-value:has(> .table) { display: block; }
|
||||
}
|
||||
|
||||
/* Table sections can sit below form wrappers; their outer card scrolls too.
|
||||
Other status cards only scroll when narrow so their headers can stick.
|
||||
Vertical overflow is clipped to prevent spurious scrolling on scaled
|
||||
Firefox displays. */
|
||||
.cbi-tblsection, .cbi-section:has(.cbi-tblsection), .cbi-section:has(> :where(#packages)) { overflow-x: auto; overflow-y: hidden; }
|
||||
/* overlay scrollbars show nothing until touched: a shade on the side with
|
||||
more table says it scrolls. The covers move with the content and hide
|
||||
the shade at either end, so a card that fits looks as before. */
|
||||
.vt-main .cbi-tblsection, .vt-main .cbi-section:has(> .cbi-tblsection) {
|
||||
.vt-main .cbi-tblsection, .vt-main .cbi-section:has(.cbi-tblsection), .vt-main .cbi-section:has(> :where(#packages)) {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--s1) 50%, transparent), linear-gradient(to left, var(--s1) 50%, transparent),
|
||||
linear-gradient(to right, var(--line-2), transparent), linear-gradient(to left, var(--line-2), transparent);
|
||||
@@ -2547,11 +2565,11 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
||||
section to scroll in: the card itself scrolls, its rows held in one
|
||||
table box (LuCI puts every row, titles too, in the tbody, so the
|
||||
columns stay aligned), its header static as in sections */
|
||||
:is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) {
|
||||
:is(#view, #view > div:not(.cbi-section), .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) {
|
||||
display: block; overflow-x: auto;
|
||||
}
|
||||
/* and says so with the same shade as the table cards (Processes) */
|
||||
.vt-main :is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) {
|
||||
.vt-main :is(#view, #view > div:not(.cbi-section), .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) {
|
||||
background-image:
|
||||
linear-gradient(to right, var(--s1) 50%, transparent), linear-gradient(to left, var(--s1) 50%, transparent),
|
||||
linear-gradient(to right, var(--line-2), transparent), linear-gradient(to left, var(--line-2), transparent);
|
||||
@@ -2560,15 +2578,15 @@ html:not([data-darkmode="true"]) .vt-login-page::before {
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
}
|
||||
:is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) > tbody { display: table; width: 100%; }
|
||||
:is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:has(.cbi-dropdown[open]) { overflow: visible; }
|
||||
:is(#view, #view > div, .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table .tr > .th.th { position: static; }
|
||||
:is(#view, #view > div:not(.cbi-section), .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:not(:has(> thead)) > tbody { display: table; width: 100%; }
|
||||
:is(#view, #view > div:not(.cbi-section), .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table:has(.cbi-dropdown[open]) { overflow: visible; }
|
||||
:is(#view, #view > div:not(.cbi-section), .cbi-map, #view > div > div > [data-tab-title]:not(.cbi-section)) > .table .tr > .th.th { position: static; }
|
||||
}
|
||||
|
||||
|
||||
/* a sticky header inside a horizontal scroller would stick relative to that
|
||||
scroller (pushed down by --stick-top over the rows): keep those static */
|
||||
.cbi-tblsection .table .tr > .th.th, .cbi-section:has(> .cbi-tblsection) .table .tr > .th.th { position: static; }
|
||||
.cbi-tblsection .table .tr > .th.th, .cbi-section:has(.cbi-tblsection) .table .tr > .th.th, #packages .tr > .th.th { position: static; }
|
||||
@media (max-width: 1180px) { .cbi-section .table .tr > .th.th { position: static; } }
|
||||
|
||||
/* add-ons: adblock and banIP inject status palettes keyed on the OS
|
||||
@@ -2887,6 +2905,8 @@ select[id^="widget.cbid.firewall."] > option { color: var(--text); font-weight:
|
||||
|
||||
/* Network -> Diagnostics: the three tools in equal columns, top aligned */
|
||||
.table:has(.diag-action) { table-layout: fixed; }
|
||||
/* LuCI measures the scroll parent before opening a diagnostic menu. */
|
||||
#view .cbi-map > .table:has(.diag-action .cbi-dropdown) { overflow: visible; }
|
||||
.table:has(.diag-action) .tr > .td { width: calc(100% / 3); vertical-align: top; }
|
||||
.table:has(.diag-action) .td > input { max-width: none; } /* fields fill them: even gaps */
|
||||
|
||||
@@ -3042,7 +3062,7 @@ select[id^="widget.cbid.firewall."] > option { color: var(--text); font-weight:
|
||||
white-space: normal; text-align: center; line-height: 1.15;
|
||||
}
|
||||
.cbi-section { overflow-x: visible; }
|
||||
.cbi-tblsection, .cbi-section:has(> .cbi-tblsection) { overflow-x: visible; }
|
||||
.cbi-tblsection, .cbi-section:has(.cbi-tblsection), .cbi-section:has(> :where(#packages)) { overflow: visible; }
|
||||
.network-status-table .ifacebox-head, .network-status-table .ifacebox-body { padding-left: 12px; padding-right: 12px; }
|
||||
:is(.ifacebox-body, .ifacebadge) > span:has(> .nowrap) { column-gap: 10px; }
|
||||
.v-meter-sub { margin-left: 0; flex-basis: 100%; }
|
||||
@@ -3114,6 +3134,7 @@ select[id^="widget.cbid.firewall."] > option { color: var(--text); font-weight:
|
||||
:is(.vt-main, .modal) .table .tr > .td[data-title]:not(:only-child) { flex-basis: 50%; }
|
||||
:is(.vt-main, .modal) .table .tr > .td[data-title]:last-child:nth-child(odd) { flex-basis: 100%; }
|
||||
:is(.vt-main, .modal) .table .tr > .td.cbi-section-actions { flex-basis: 100%; text-align: left !important; }
|
||||
:is(.vt-main, .modal) .table .tr > .td:not(.cbi-value-field) { white-space: normal; overflow-wrap: anywhere; }
|
||||
/* a named grid section's name cell heads its card */
|
||||
:is(.vt-main, .modal) .table .tr > .td.cbi-section-table-titles { flex-basis: 100%; color: var(--text); font-weight: 600; }
|
||||
:is(.vt-main, .modal) .table .tr > .td:is([data-name="_ifacebox"], [data-name="_ifacestat"]) { flex-basis: 100%; }
|
||||
@@ -3226,7 +3247,7 @@ select[id^="widget.cbid.firewall."] > option { color: var(--text); font-weight:
|
||||
.spinning::before { animation-duration: 1.6s !important; animation-iteration-count: infinite !important; }
|
||||
.alert-message { transition-duration: .01s; }
|
||||
/* ui.Dropdown moves focus on transitionend: keep a (near-instant) transition */
|
||||
.cbi-dropdown[open] > ul.dropdown { transition-duration: .01s; }
|
||||
.cbi-dropdown[open] > ul.dropdown { transition-duration: .001s; }
|
||||
.cbi-progressbar > div { transition: none; }
|
||||
.vt-crumb-switch > svg, .vt-rail-caret, .vt-rail-pin svg { transition: none; }
|
||||
.v-nft-card > summary::before, .v-nft-jump::after { transition: none; }
|
||||
|
||||
Reference in new issue
Block a user