mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-05 01:44:24 +08:00
update 2026-11-01 17:41:31
This commit is contained in:
1 parent
98d078dd48
commit
1afa674165
62 files changed
+3735
-2921
No files matched your search
@@ -1,70 +0,0 @@
|
||||
name: build-apk
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'dev-*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
SDK_DIR: /builder
|
||||
PKG_DIR_NAME: luci-app-adguardhome-tk
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
matrix:
|
||||
sdk: [x86-64-25.12.6]
|
||||
container:
|
||||
image: ghcr.io/openwrt/sdk:${{ matrix.sdk }}
|
||||
options: --user root
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 缓存依赖源码下载
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{ env.SDK_DIR }}/dl
|
||||
key: adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml', 'scripts/*.sh') }}
|
||||
restore-keys: |
|
||||
adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-
|
||||
|
||||
- name: 准备 SDK
|
||||
run: bash scripts/prepare-sdk.sh
|
||||
|
||||
- name: 构建包
|
||||
run: runuser -u buildbot -- bash scripts/build-apk.sh
|
||||
|
||||
- name: 整理 APK 产物
|
||||
run: bash scripts/collect-apks.sh
|
||||
|
||||
- name: 上传 APK 压缩包
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: adguardhome-apks
|
||||
path: dist/*.apk
|
||||
archive: true
|
||||
if-no-files-found: error
|
||||
|
||||
- name: 发布到 Releases
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
prerelease: ${{ startsWith(github.ref_name, 'dev-') }}
|
||||
make_latest: 'false'
|
||||
fail_on_unmatched_files: true
|
||||
files: dist/*.apk
|
||||
@@ -1,2 +1,9 @@
|
||||
.DS_Store
|
||||
.local
|
||||
|
||||
# Local debug artifacts from playwright-cli UI verification
|
||||
adg-*.png
|
||||
m-*.png
|
||||
bs-*.png
|
||||
*.png
|
||||
!screenshots/*.png
|
||||
.playwright-cli/
|
||||
@@ -1,109 +1,79 @@
|
||||
# Copyright (C) 2018-2024 OpenWrt luci-app-adguardhome contributors
|
||||
#
|
||||
# Copyright (C) 2025 w9315273
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-adguardhome-tk
|
||||
PKG_VERSION:=2.6.0
|
||||
PKG_RELEASE:=3
|
||||
PKG_NAME:=luci-app-adguardhome
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_MAINTAINER:=w9315273
|
||||
PKG_BUILD_DEPENDS:=luci-base/host
|
||||
LUCI_TITLE:=LuCI app for AdGuardHome
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:=+ca-certs +curl +wget-ssl +PACKAGE_$(PKG_NAME)_INCLUDE_binary:adguardhome
|
||||
# luci-compat only exists in ucode-based LuCI (24.10+)
|
||||
ifneq ($(wildcard $(TOPDIR)/feeds/luci/luci-compat/Makefile),)
|
||||
LUCI_DEPENDS+= +luci-compat
|
||||
endif
|
||||
LUCI_DESCRIPTION:=LuCI support for AdGuardHome
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/luci-app-adguardhome-tk
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LuCI app for AdGuardHome
|
||||
PKG_MAINTAINER:=https://github.com/w9315273/luci-app-adguardhome
|
||||
PKGARCH:=all
|
||||
PROVIDES:=luci-app-adguardhome
|
||||
CONFLICTS:=luci-app-adguardhome
|
||||
DEPENDS:=+luci-base +rpcd-mod-ucode +uclient-fetch +jsonfilter +flock +ca-bundle +!(PACKAGE_libustream-openssl||PACKAGE_libustream-wolfssl):libustream-mbedtls +tar
|
||||
define Package/$(PKG_NAME)/config
|
||||
config PACKAGE_$(PKG_NAME)_INCLUDE_binary
|
||||
bool "Include Binary File"
|
||||
default n
|
||||
help
|
||||
When enabled, also pulls in the adguardhome binary package.
|
||||
Disabled by default to avoid init.d/config name conflict with
|
||||
the adguardhome package (uses lowercase init.d/adguardhome,
|
||||
while this LuCI app uses /etc/init.d/AdGuardHome). The LuCI
|
||||
app downloads the binary at runtime, so the package dependency
|
||||
is only needed for offline / sysupgrade scenarios.
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/description
|
||||
LuCI support for AdGuardHome (nftables-based DNS redirect).
|
||||
This package does NOT include the AdGuardHome core binary.
|
||||
endef
|
||||
PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_binary
|
||||
|
||||
define Package/luci-i18n-adguardhome-tk-zh-cn
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
HIDDEN:=1
|
||||
TITLE:=AdGuardHome - Simplified Chinese translation
|
||||
PKGARCH:=all
|
||||
PROVIDES:=luci-i18n-adguardhome-zh-cn
|
||||
CONFLICTS:=luci-i18n-adguardhome-zh-cn
|
||||
DEPENDS:=luci-app-adguardhome-tk
|
||||
DEFAULT:=LUCI_LANG_zh_Hans
|
||||
endef
|
||||
|
||||
define Package/luci-i18n-adguardhome-tk-zh-cn/description
|
||||
Simplified Chinese translation for luci-app-adguardhome-tk.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/conffiles
|
||||
define Package/luci-app-adguardhome/conffiles
|
||||
/usr/share/AdGuardHome/links.txt
|
||||
/etc/config/AdGuardHome
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/install
|
||||
cp -pR ./root/* $(1)/
|
||||
endef
|
||||
|
||||
define Package/luci-i18n-adguardhome-tk-zh-cn/install
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
echo "uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'; uci commit luci" \
|
||||
> $(1)/etc/uci-defaults/luci-i18n-adguardhome-tk-zh-cn
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
|
||||
po2lmo ./po/zh-cn/AdGuardHome.po \
|
||||
$(1)/usr/lib/lua/luci/i18n/AdGuardHome.zh-cn.lmo
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/postinst
|
||||
define Package/luci-app-adguardhome/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
|
||||
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
|
||||
|
||||
enable=$$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
|
||||
if [ "$$enable" = "1" ]; then
|
||||
/etc/init.d/AdGuardHome reload
|
||||
fi
|
||||
|
||||
# Remove the former menu definition when upgrading.
|
||||
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -f /tmp/luci-modulecache/*
|
||||
/etc/init.d/rpcd reload
|
||||
|
||||
# Stop and disable the lowercase /etc/init.d/adguardhome that ships
|
||||
# with the adguardhome binary package, so it can not race the LuCI
|
||||
# app's /etc/init.d/AdGuardHome (issue #254).
|
||||
if [ -x /etc/init.d/adguardhome ] && [ -x /etc/init.d/AdGuardHome ]; then
|
||||
/etc/init.d/adguardhome stop >/dev/null 2>&1
|
||||
/etc/init.d/adguardhome disable >/dev/null 2>&1
|
||||
fi
|
||||
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
|
||||
enable="$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)"
|
||||
[ "$enable" = "1" ] && /etc/init.d/AdGuardHome reload >/dev/null 2>&1
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -f /tmp/luci-modulecache/* 2>/dev/null
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome-tk/prerm
|
||||
define Package/luci-app-adguardhome/prerm
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
/etc/init.d/AdGuardHome disable
|
||||
/etc/init.d/AdGuardHome stop 2>/dev/null || true
|
||||
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete ucitrack.@AdGuardHome[-1]
|
||||
commit ucitrack
|
||||
EOF
|
||||
/etc/init.d/AdGuardHome disable >/dev/null 2>&1
|
||||
/etc/init.d/AdGuardHome stop >/dev/null 2>&1
|
||||
ucitrack_del() {
|
||||
local name="$1"
|
||||
local track="$(uci show ucitrack 2>/dev/null | grep -F "$name\." | head -1)"
|
||||
[ -n "$track" ] && uci -q del "$track" 2>/dev/null
|
||||
}
|
||||
ucitrack_del "AdGuardHome"
|
||||
[ -n "$(uci show ucitrack 2>/dev/null | grep -c 'AdGuardHome')" ] || true
|
||||
uci commit ucitrack 2>/dev/null
|
||||
fi
|
||||
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-adguardhome-tk))
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn))
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
$(eval $(call BuildPackage,luci-app-adguardhome))
|
||||
@@ -0,0 +1,81 @@
|
||||
# luci-app-adguardhome
|
||||
|
||||
适用于 OpenWrt / ImmortalWrt 的 AdGuard Home LuCI 控制面板。
|
||||
|
||||
> 基于 [rufengsuixing/luci-app-adguardhome](https://github.com/rufengsuixing/luci-app-adguardhome) 维护,
|
||||
> 针对 **ImmortalWrt 24.10+** 进行了大量 UI 重构和 Bug 修复。
|
||||
|
||||
## 兼容性
|
||||
|
||||
| 系统 | 状态 |
|
||||
|------|------|
|
||||
| ImmortalWrt 24.10 / 25.12 | 主要测试目标,完全兼容 |
|
||||
| OpenWrt 23.05 | 兼容 |
|
||||
| OpenWrt 21.02 / 22.03 | 兼容 |
|
||||
| OpenWrt 19.07 | 兼容(jail 字段回退为 TextValue) |
|
||||
| OpenWrt 18.06 | 基本兼容(DynamicList 自动回退) |
|
||||
|
||||
## 截图
|
||||
|
||||
### 概览
|
||||
|
||||

|
||||
|
||||
### 基础设置
|
||||
|
||||

|
||||
|
||||
### 运维
|
||||
|
||||

|
||||
|
||||
### 日志
|
||||
|
||||

|
||||
|
||||
### 手动配置
|
||||
|
||||

|
||||
|
||||
## 功能
|
||||
|
||||
- **概览面板**:服务状态、重定向模式、核心版本、一键启停 + Web 控制台快捷入口
|
||||
- **基础设置**:启用开关、管理端口、核心更新、5553 重定向(4 种模式)、路径配置、进程参数
|
||||
- **工具与任务**:改密、sysupgrade 保留文件、关机备份、Crontab 计划任务
|
||||
- **日志查看**:实时轮询、反向/本地时区切换、下载、清空
|
||||
- **手动配置**:YAML 编辑器(CodeMirror 可选),保存前二次确认
|
||||
- **核心自动更新**:GitHub/AdGuard 多源下载 → UPX 压缩 → 重启服务
|
||||
- **UPX 压缩**:>8MB 二进制自动压缩(支持 x86_64 / arm64 / mips 等架构)
|
||||
- **计划任务**:自动更新核心、切割日志、IPv6 hosts 同步
|
||||
- **升级保护**:sysupgrade 保留文件 + 关机备份
|
||||
|
||||
## 安装
|
||||
|
||||
### Feed 方式
|
||||
|
||||
```bash
|
||||
echo 'src-git adguardhome https://github.com/kenzok78/luci-app-adguardhome' >> feeds.conf.default
|
||||
./scripts/feeds update -a && ./scripts/feeds install -a -p adguardhome
|
||||
make package/luci-app-adguardhome/compile V=s
|
||||
```
|
||||
|
||||
### 直接编译
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kenzok78/luci-app-adguardhome.git package/luci-app-adguardhome
|
||||
make menuconfig # LuCI → Applications → luci-app-adguardhome
|
||||
make -j$(nproc) V=s
|
||||
```
|
||||
|
||||
## 重定向模式
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| 不启用 | 仅运行,不拦截 DNS |
|
||||
| 作为 dnsmasq 上游 | dnsmasq 转发到 AGH |
|
||||
| 53 端口劫持 | iptables 重定向到 AGH |
|
||||
| 替代 dnsmasq | AGH 直接监听 53 |
|
||||
|
||||
## 许可证
|
||||
|
||||
Apache-2.0
|
||||
@@ -1,35 +0,0 @@
|
||||
# AdGuard Home for LuCI
|
||||
|
||||
Manage AdGuard Home in OpenWrt's LuCI interface. LuCI pages are rewritten in JavaScript, with an rpcd + ucode backend and nftables-based DNS forwarding.
|
||||
|
||||
[简体中文](README.md) · [English](README.en.md)
|
||||
|
||||
## Features
|
||||
|
||||
- Service status and core updates
|
||||
- DNS handling through firewall forwarding, dnsmasq upstream, or port 53 replacement
|
||||
- YAML configuration editing, validation, and web login password changes
|
||||
- Runtime logs with reverse order, local timestamps, download, and a verbose logging toggle
|
||||
|
||||
> [!TIP]
|
||||
> **Quick installation**
|
||||
>
|
||||
> Run in the terminal of an OpenWrt router using the APK package manager:
|
||||
>
|
||||
> ```sh
|
||||
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh
|
||||
> ```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Basic settings
|
||||
|
||||

|
||||
|
||||
### Manual configuration
|
||||
|
||||

|
||||
|
||||
### Logs
|
||||
|
||||

|
||||
@@ -1,35 +0,0 @@
|
||||
# AdGuard Home for LuCI
|
||||
|
||||
在 OpenWrt 的 LuCI 界面中管理 AdGuard Home。使用 JavaScript 重写 LuCI 页面,后端采用 rpcd + ucode,并适配 nftables DNS 转发。
|
||||
|
||||
[简体中文](README.md) · [English](README.en.md)
|
||||
|
||||
## 功能
|
||||
|
||||
- 服务状态查看与核心更新
|
||||
- 防火墙转发、dnsmasq 上游、替代 dnsmasq 使用 53 端口等 DNS 接入方式
|
||||
- YAML 配置编辑与校验、网页登录密码修改
|
||||
- 运行日志查看,支持逆序、本地时间、下载和详细日志开关
|
||||
|
||||
> [!TIP]
|
||||
> **一键安装**
|
||||
>
|
||||
> 在使用 APK 包管理器的 OpenWrt 路由器终端执行:
|
||||
>
|
||||
> ```sh
|
||||
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh zh-cn
|
||||
> ```
|
||||
|
||||
## 截图
|
||||
|
||||
### 基础设置
|
||||
|
||||

|
||||
|
||||
### 手动设置
|
||||
|
||||

|
||||
|
||||
### 日志
|
||||
|
||||

|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 227 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 169 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 164 KiB |
@@ -0,0 +1,360 @@
|
||||
/* luci-app-adguardhome card-style theme (inspired by luci-app-clashoo) */
|
||||
.adg-wrap {
|
||||
--adg-primary-rgb: 0, 122, 255;
|
||||
--adg-primary: rgba(var(--adg-primary-rgb), 0.9);
|
||||
--adg-card-bg: rgba(128, 128, 128, 0.04);
|
||||
--adg-card-border: rgba(128, 128, 128, 0.16);
|
||||
--adg-card-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
--adg-card-shadow-hover: 0 6px 24px rgba(0, 0, 0, 0.07);
|
||||
--adg-radius: 12px;
|
||||
--adg-label-muted: rgba(60, 66, 76, 0.85);
|
||||
--adg-input-bg: rgba(128, 128, 128, 0.05);
|
||||
--adg-input-border: rgba(128, 128, 128, 0.2);
|
||||
--adg-ok: #2db84a;
|
||||
--adg-warn: #e0a02f;
|
||||
--adg-bad: #d05050;
|
||||
}
|
||||
|
||||
/* Dark mode: triggered either by OS preference, or by an explicit class
|
||||
set from head.htm when the active LuCI theme renders a dark background
|
||||
regardless of OS preference (Argon mode=dark, bootstrap-dark, etc.) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.adg-wrap {
|
||||
--adg-card-bg: rgba(255, 255, 255, 0.05);
|
||||
--adg-card-border: rgba(255, 255, 255, 0.16);
|
||||
--adg-input-bg: rgba(255, 255, 255, 0.06);
|
||||
--adg-input-border: rgba(255, 255, 255, 0.22);
|
||||
--adg-label-muted: rgba(220, 226, 232, 0.88);
|
||||
}
|
||||
}
|
||||
.adg-wrap.adg-dark {
|
||||
--adg-card-bg: rgba(255, 255, 255, 0.05);
|
||||
--adg-card-border: rgba(255, 255, 255, 0.16);
|
||||
--adg-input-bg: rgba(255, 255, 255, 0.06);
|
||||
--adg-input-border: rgba(255, 255, 255, 0.22);
|
||||
--adg-label-muted: rgba(220, 226, 232, 0.88);
|
||||
}
|
||||
/* LuCI's selected theme is authoritative. If the OS/browser prefers dark but
|
||||
Argon/bootstrap is in light mode, keep contrast tuned for a light surface. */
|
||||
.adg-wrap:not(.adg-dark) {
|
||||
--adg-card-bg: rgba(255, 255, 255, 0.45);
|
||||
--adg-card-border: rgba(50, 50, 93, 0.12);
|
||||
--adg-input-bg: rgba(255, 255, 255, 0.55);
|
||||
--adg-input-border: rgba(50, 50, 93, 0.18);
|
||||
--adg-label-muted: rgba(50, 50, 93, 0.78);
|
||||
color: #32325d;
|
||||
}
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-description,
|
||||
.adg-wrap:not(.adg-dark) .cbi-section-descr,
|
||||
.adg-wrap:not(.adg-dark) .cbi-map-descr,
|
||||
.adg-wrap:not(.adg-dark) .adg-ov-card .title,
|
||||
.adg-wrap:not(.adg-dark) .adg-ov-card .sub {
|
||||
color: rgba(50, 50, 93, 0.70) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.adg-wrap:not(.adg-dark) .cbi-value > .cbi-value-title,
|
||||
.adg-wrap:not(.adg-dark) .cbi-checkbox {
|
||||
color: rgba(50, 50, 93, 0.82) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field input[type="text"],
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field input[type="password"],
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field input[type="number"],
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field select,
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field textarea {
|
||||
color: #32325d !important;
|
||||
-webkit-text-fill-color: #32325d;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field input:disabled,
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field select:disabled,
|
||||
.adg-wrap:not(.adg-dark) .cbi-value-field textarea:disabled {
|
||||
color: #5f6f86 !important;
|
||||
-webkit-text-fill-color: #5f6f86;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
/* Some Argon dark rules set transparent input bg with !important — override
|
||||
so our inputs stay visible. */
|
||||
.adg-wrap.adg-dark .cbi-value-field input[type="text"],
|
||||
.adg-wrap.adg-dark .cbi-value-field input[type="password"],
|
||||
.adg-wrap.adg-dark .cbi-value-field input[type="number"],
|
||||
.adg-wrap.adg-dark .cbi-value-field select,
|
||||
.adg-wrap.adg-dark .cbi-value-field textarea {
|
||||
background: var(--adg-input-bg) !important;
|
||||
border-color: var(--adg-input-border) !important;
|
||||
color: #e6eaf0 !important;
|
||||
}
|
||||
/* Help text + section descriptions — Argon paints them #ccc, bootstrap-dark
|
||||
leaves them as default gray-on-dark which is also low-contrast. */
|
||||
.adg-wrap.adg-dark .cbi-value-description,
|
||||
.adg-wrap.adg-dark .cbi-section-descr,
|
||||
.adg-wrap.adg-dark .cbi-map-descr {
|
||||
color: rgba(220, 226, 232, 0.78) !important;
|
||||
}
|
||||
.adg-wrap.adg-dark .cbi-value > .cbi-value-title {
|
||||
color: var(--adg-label-muted) !important;
|
||||
}
|
||||
.adg-wrap.adg-dark .cbi-section > h3,
|
||||
.adg-wrap.adg-dark .cbi-section > legend {
|
||||
color: #e6eaf0 !important;
|
||||
}
|
||||
|
||||
/* Cards are constrained for comfortable reading; form fills its natural width.
|
||||
Bootstrap theme has sidebar; Argon is full-width. Let the theme decide the
|
||||
outer container width — we only cap the card content. */
|
||||
.adg-wrap .cbi-section {
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
/* every CBI section becomes a card */
|
||||
.adg-wrap .cbi-section {
|
||||
background: var(--adg-card-bg);
|
||||
border: 1px solid var(--adg-card-border);
|
||||
border-radius: var(--adg-radius);
|
||||
box-shadow: var(--adg-card-shadow);
|
||||
padding: 18px 22px;
|
||||
margin: 16px 0;
|
||||
transition: box-shadow .2s ease;
|
||||
}
|
||||
.adg-wrap .cbi-section:hover {
|
||||
box-shadow: var(--adg-card-shadow-hover);
|
||||
}
|
||||
|
||||
/* section heading inside card */
|
||||
.adg-wrap .cbi-section > h3,
|
||||
.adg-wrap .cbi-section > legend {
|
||||
background: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 0 12px 0 !important;
|
||||
margin: 0 0 12px 0 !important;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .2px;
|
||||
border-bottom: 1px solid var(--adg-card-border) !important;
|
||||
}
|
||||
|
||||
/* label column slimmer (default LuCI is ~280px) */
|
||||
.adg-wrap .cbi-value > .cbi-value-title {
|
||||
width: 170px;
|
||||
min-width: 170px;
|
||||
padding-right: 12px;
|
||||
color: var(--adg-label-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* inputs */
|
||||
.adg-wrap .cbi-value-field input[type="text"],
|
||||
.adg-wrap .cbi-value-field input[type="password"],
|
||||
.adg-wrap .cbi-value-field input[type="number"],
|
||||
.adg-wrap .cbi-value-field select,
|
||||
.adg-wrap .cbi-value-field textarea {
|
||||
background: var(--adg-input-bg);
|
||||
border: 1px solid var(--adg-input-border);
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
.adg-wrap .cbi-value-field textarea {
|
||||
max-width: 420px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
/* long-form textareas need full width (URL lists / YAML editor) */
|
||||
.adg-wrap .cbi-value-field textarea[id$="downloadlinks"],
|
||||
.adg-wrap .cbi-value-field textarea[id$="escconf"],
|
||||
.adg-wrap .cbi-value-field textarea#cbid\.logview\.1\.conf {
|
||||
max-width: 100%;
|
||||
}
|
||||
.adg-wrap .cbi-value-field input:focus,
|
||||
.adg-wrap .cbi-value-field select:focus,
|
||||
.adg-wrap .cbi-value-field textarea:focus {
|
||||
border-color: var(--adg-primary);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(var(--adg-primary-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* help text */
|
||||
.adg-wrap .cbi-value-description {
|
||||
color: var(--adg-label-muted);
|
||||
font-size: 12.5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* between-row separator gone (the card itself is the boundary) */
|
||||
.adg-wrap .cbi-value {
|
||||
border-bottom: 0 !important;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* MultiValue checkbox groups — let items flow inline and wrap naturally */
|
||||
.adg-wrap .cbi-value-field .cbi-checkbox {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.adg-wrap .cbi-value-field .cbi-checkbox input[type="checkbox"] {
|
||||
width: auto;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* DynamicList — compact row layout */
|
||||
.adg-wrap .cbi-dynlist .cbi-value-title { display: none; }
|
||||
.adg-wrap .cbi-dynlist .cbi-value { padding: 4px 0; }
|
||||
.adg-wrap .cbi-dynlist .cbi-value-field { flex: 1; max-width: 420px; }
|
||||
.adg-wrap .cbi-dynlist .cbi-value-field input[type="text"] { max-width: 100%; }
|
||||
|
||||
/* AdGuardHome custom status row */
|
||||
.adg-status-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 6px 0 18px 0;
|
||||
}
|
||||
.adg-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--adg-card-bg);
|
||||
border: 1px solid var(--adg-card-border);
|
||||
font-size: 13px;
|
||||
color: var(--adg-label-muted);
|
||||
}
|
||||
.adg-status-pill .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--adg-label-muted);
|
||||
}
|
||||
.adg-status-pill.ok .dot { background: var(--adg-ok); }
|
||||
.adg-status-pill.warn .dot { background: var(--adg-warn); }
|
||||
.adg-status-pill.bad .dot { background: var(--adg-bad); }
|
||||
.adg-status-pill.ok { color: var(--adg-ok); border-color: rgba(45,184,74,.3); }
|
||||
.adg-status-pill.bad { color: var(--adg-bad); border-color: rgba(208,80,80,.3); }
|
||||
|
||||
/* CBI button color fallback — ensure visibility on themes that use
|
||||
transparent backgrounds (e.g. Argon light) */
|
||||
.adg-wrap input.cbi-button-apply,
|
||||
.adg-wrap button.cbi-button-apply {
|
||||
background: #007aff !important;
|
||||
border-color: #007aff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.adg-wrap input.cbi-button-apply:hover,
|
||||
.adg-wrap button.cbi-button-apply:hover {
|
||||
background: #0064d2 !important;
|
||||
border-color: #0064d2 !important;
|
||||
}
|
||||
.adg-wrap input.cbi-button-remove,
|
||||
.adg-wrap button.cbi-button-remove {
|
||||
background: #d05050 !important;
|
||||
border-color: #d05050 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.adg-wrap input.cbi-button-remove:hover,
|
||||
.adg-wrap button.cbi-button-remove:hover {
|
||||
background: #b03d3d !important;
|
||||
border-color: #b03d3d !important;
|
||||
}
|
||||
.adg-wrap input.cbi-button-save,
|
||||
.adg-wrap button.cbi-button-save {
|
||||
background: #2d7d46 !important;
|
||||
border-color: #2d7d46 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.adg-wrap input.cbi-button-save:hover,
|
||||
.adg-wrap button.cbi-button-save:hover {
|
||||
background: #226434 !important;
|
||||
border-color: #226434 !important;
|
||||
}
|
||||
|
||||
/* collapsible sections */
|
||||
.adg-wrap .cbi-section.adg-collapsible > h3,
|
||||
.adg-wrap .cbi-section.adg-collapsible > legend {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 28px !important;
|
||||
user-select: none;
|
||||
}
|
||||
.adg-wrap .cbi-section.adg-collapsible > h3::after,
|
||||
.adg-wrap .cbi-section.adg-collapsible > legend::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-right: 2px solid var(--adg-label-muted);
|
||||
border-bottom: 2px solid var(--adg-label-muted);
|
||||
transform: translateY(-65%) rotate(45deg);
|
||||
transition: transform .2s ease;
|
||||
}
|
||||
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > h3::after,
|
||||
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > legend::after {
|
||||
transform: translateY(-30%) rotate(-45deg);
|
||||
}
|
||||
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > .cbi-value,
|
||||
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > div.cbi-section-node,
|
||||
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > fieldset {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* mobile tweaks — reset table/flex layout entirely */
|
||||
@media (max-width: 720px) {
|
||||
.adg-wrap .cbi-section {
|
||||
padding: 12px 10px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
/* force flex column, kill any theme table layout */
|
||||
.adg-wrap .cbi-value {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-bottom: 0 !important;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.adg-wrap .cbi-value > .cbi-value-title {
|
||||
width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
padding: 0 0 3px 0 !important;
|
||||
flex: none;
|
||||
text-align: left;
|
||||
}
|
||||
.adg-wrap .cbi-value > .cbi-value-field {
|
||||
width: 100% !important;
|
||||
flex: none;
|
||||
}
|
||||
.adg-wrap .cbi-value-field input[type="text"],
|
||||
.adg-wrap .cbi-value-field input[type="password"],
|
||||
.adg-wrap .cbi-value-field input[type="number"],
|
||||
.adg-wrap .cbi-value-field select {
|
||||
max-width: 100%;
|
||||
}
|
||||
.adg-wrap .cbi-value-field > div {
|
||||
width: 100%;
|
||||
}
|
||||
/* checkboxes — single column */
|
||||
.adg-wrap .cbi-value-field .cbi-checkbox {
|
||||
display: flex;
|
||||
white-space: normal;
|
||||
margin-right: 0;
|
||||
margin-bottom: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
/* DynamicList — expand */
|
||||
.adg-wrap .cbi-dynlist .cbi-value-field { max-width: 100%; }
|
||||
/* hide tooltips on touch */
|
||||
.adg-wrap .cbi-tooltip-container,
|
||||
.adg-wrap .cbi-tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// NOTE: luci-app-adguardhome 使用多页面 CBI 模板架构,luci 23.05+ 通过 menu.d alias 跳转至 base 子页。
|
||||
// 此 JS 文件不直接调用,保留供参考。
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require form';
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
// 实际界面由 luasrc/model/cbi/AdGuardHome/ 下多个 CBI 模型提供
|
||||
return E('p', {}, _('Loading…'));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
module("luci.controller.AdGuardHome", package.seeall)
|
||||
|
||||
local fs = require "nixio.fs"
|
||||
local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local function service_running()
|
||||
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
|
||||
|
||||
if not fs.access(binpath) then
|
||||
return false
|
||||
end
|
||||
|
||||
return luci.sys.call("/etc/init.d/AdGuardHome status >/dev/null 2>&1") == 0
|
||||
end
|
||||
|
||||
function index()
|
||||
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
|
||||
if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-adguardhome.json") then
|
||||
local page = entry({"admin", "services", "AdGuardHome"},
|
||||
alias("admin", "services", "AdGuardHome", "overview"),
|
||||
_("AdGuard Home"), 10)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-adguardhome" }
|
||||
|
||||
entry({"admin", "services", "AdGuardHome", "overview"},
|
||||
cbi("AdGuardHome/overview"), _("概览"), 1).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "base"},
|
||||
cbi("AdGuardHome/base"), _("基础设置"), 2).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "tools"},
|
||||
cbi("AdGuardHome/tools"), _("运维"), 3).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "log"},
|
||||
form("AdGuardHome/log"), _("日志"), 4).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "manual"},
|
||||
cbi("AdGuardHome/manual"), _("手动配置"), 5).leaf = true
|
||||
end
|
||||
|
||||
-- API 路由在新旧版本均需注册
|
||||
entry({"admin", "services", "AdGuardHome", "status"},
|
||||
call("act_status"), nil).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "toggle"},
|
||||
call("toggle_service"), nil).leaf = true
|
||||
entry({"admin", "services", "AdGuardHome", "check"},
|
||||
call("check_update"), nil)
|
||||
entry({"admin", "services", "AdGuardHome", "doupdate"},
|
||||
call("do_update"), nil)
|
||||
entry({"admin", "services", "AdGuardHome", "getlog"},
|
||||
call("get_log"), nil)
|
||||
entry({"admin", "services", "AdGuardHome", "dodellog"},
|
||||
call("do_dellog"), nil)
|
||||
entry({"admin", "services", "AdGuardHome", "reloadconfig"},
|
||||
call("reload_config"), nil)
|
||||
entry({"admin", "services", "AdGuardHome", "gettemplateconfig"},
|
||||
call("get_template_config"), nil)
|
||||
end
|
||||
|
||||
local function gen_template_config()
|
||||
local dns_servers = ""
|
||||
local resolv_auto = "/tmp/resolv.conf.d/resolv.conf.auto"
|
||||
if fs.access(resolv_auto) then
|
||||
for line in io.lines(resolv_auto) do
|
||||
local ns = line:match("^[^#]*nameserver%s+([^%s]+)")
|
||||
if ns then
|
||||
dns_servers = dns_servers .. " - " .. ns .. "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local tmpl = "/usr/share/AdGuardHome/AdGuardHome_template.yaml"
|
||||
local f = io.open(tmpl, "r")
|
||||
if not f then return "" end
|
||||
|
||||
local lines = {}
|
||||
for line in f:lines() do
|
||||
if line == "#bootstrap_dns" then
|
||||
table.insert(lines, dns_servers)
|
||||
elseif line == "#upstream_dns" then
|
||||
table.insert(lines, dns_servers)
|
||||
else
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
function get_template_config()
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
http.write(gen_template_config())
|
||||
end
|
||||
|
||||
function reload_config()
|
||||
fs.remove("/tmp/AdGuardHometmpconfig.yaml")
|
||||
http.prepare_content("application/json")
|
||||
http.write('{}')
|
||||
end
|
||||
|
||||
function act_status()
|
||||
local result = {}
|
||||
result.running = service_running()
|
||||
|
||||
local redir = fs.readfile("/var/run/AdGredir")
|
||||
result.redirect = (redir == "1")
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(result)
|
||||
end
|
||||
|
||||
function toggle_service()
|
||||
local enabled = http.formvalue("enabled") == "1" and "1" or "0"
|
||||
local old_enabled = uci:get("AdGuardHome", "AdGuardHome", "enabled") == "1" and "1" or "0"
|
||||
local result = {
|
||||
enabled = (enabled == "1")
|
||||
}
|
||||
|
||||
-- Preflight: when enabling, make sure the binary is actually present.
|
||||
-- Without this, init.d reload silently fails and the UI only sees an
|
||||
-- opaque "Operation failed" toast (issue #254).
|
||||
if enabled == "1" then
|
||||
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
|
||||
if not fs.access(binpath) then
|
||||
result.success = false
|
||||
result.enabled = (old_enabled == "1")
|
||||
result.running = false
|
||||
result.message = "找不到 AdGuardHome 二进制文件(" .. binpath ..
|
||||
"),请先在「运维」页面下载。"
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(result)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
uci:set("AdGuardHome", "AdGuardHome", "enabled", enabled)
|
||||
uci:commit("AdGuardHome")
|
||||
|
||||
local rc
|
||||
if enabled == "1" then
|
||||
-- Detach the reload from the CGI request so the HTTP call returns
|
||||
-- immediately while the daemon comes up. Fully redirecting fds + `&`
|
||||
-- is enough — avoid start-stop-daemon, whose busybox applet is not
|
||||
-- enabled on some images (e.g. immortalwrt 25.10), where it would
|
||||
-- exit 127 and make the toggle wrongly report a start failure (#254).
|
||||
rc = luci.sys.call("/etc/init.d/AdGuardHome reload >/dev/null 2>&1 &")
|
||||
else
|
||||
rc = luci.sys.call("/etc/init.d/AdGuardHome stop >/dev/null 2>&1")
|
||||
end
|
||||
|
||||
if enabled == "1" then
|
||||
result.running = false
|
||||
else
|
||||
result.running = service_running()
|
||||
for _ = 1, 2 do
|
||||
if not result.running then
|
||||
break
|
||||
end
|
||||
luci.sys.call("sleep 1")
|
||||
result.running = service_running()
|
||||
end
|
||||
end
|
||||
|
||||
result.pending = (enabled == "1" and not result.running)
|
||||
result.success = (rc == 0) and (enabled == "1" or not result.running)
|
||||
|
||||
if not result.success then
|
||||
uci:set("AdGuardHome", "AdGuardHome", "enabled", old_enabled)
|
||||
uci:commit("AdGuardHome")
|
||||
result.enabled = (old_enabled == "1")
|
||||
result.message = enabled == "1" and "AdGuardHome start failed" or "AdGuardHome stop failed"
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(result)
|
||||
end
|
||||
|
||||
function do_update()
|
||||
fs.writefile("/var/run/lucilogpos", "0")
|
||||
http.prepare_content("application/json")
|
||||
http.write('{}')
|
||||
|
||||
local arg = ""
|
||||
if luci.http.formvalue("force") == "1" then
|
||||
arg = "force"
|
||||
end
|
||||
|
||||
local script = "/usr/share/AdGuardHome/update_core.sh"
|
||||
if fs.access("/var/run/update_core") then
|
||||
if arg == "force" then
|
||||
-- Security fix: script 路径是固定常量,arg 只能是 "force" 或 "",无注入风险
|
||||
luci.sys.exec("pkill -f '" .. script .. "' 2>/dev/null; " .. script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
|
||||
end
|
||||
else
|
||||
luci.sys.exec(script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
|
||||
end
|
||||
end
|
||||
|
||||
function get_log()
|
||||
local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile")
|
||||
|
||||
if not logfile or logfile == "" then
|
||||
http.write("no log available\n")
|
||||
return
|
||||
end
|
||||
|
||||
if logfile == "syslog" then
|
||||
if not fs.access("/var/run/AdGuardHomesyslog") then
|
||||
luci.sys.exec("(/usr/share/AdGuardHome/getsyslog.sh &); sleep 1;")
|
||||
end
|
||||
logfile = "/tmp/AdGuardHometmp.log"
|
||||
fs.writefile("/var/run/AdGuardHomesyslog", "1")
|
||||
elseif not fs.access(logfile) then
|
||||
http.write("")
|
||||
return
|
||||
end
|
||||
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
|
||||
local fdp = 0
|
||||
if fs.access("/var/run/lucilogreload") then
|
||||
fdp = 0
|
||||
fs.remove("/var/run/lucilogreload")
|
||||
else
|
||||
local pos = fs.readfile("/var/run/lucilogpos")
|
||||
fdp = tonumber(pos) or 0
|
||||
end
|
||||
|
||||
local f = io.open(logfile, "r")
|
||||
if not f then
|
||||
http.write("")
|
||||
return
|
||||
end
|
||||
|
||||
f:seek("set", fdp)
|
||||
local content = f:read(2048000) or ""
|
||||
fdp = f:seek()
|
||||
f:close()
|
||||
|
||||
fs.writefile("/var/run/lucilogpos", tostring(fdp))
|
||||
http.write(content)
|
||||
end
|
||||
|
||||
function do_dellog()
|
||||
local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile")
|
||||
if logfile and logfile ~= "" and logfile ~= "syslog" then
|
||||
fs.writefile(logfile, "")
|
||||
end
|
||||
http.prepare_content("application/json")
|
||||
http.write('{}')
|
||||
end
|
||||
|
||||
function check_update()
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
|
||||
local pos = fs.readfile("/var/run/lucilogpos")
|
||||
local fdp = tonumber(pos) or 0
|
||||
|
||||
local f = io.open("/tmp/AdGuardHome_update.log", "r")
|
||||
if not f then
|
||||
http.write("")
|
||||
return
|
||||
end
|
||||
|
||||
f:seek("set", fdp)
|
||||
local content = f:read(2048000) or ""
|
||||
fdp = f:seek()
|
||||
f:close()
|
||||
|
||||
fs.writefile("/var/run/lucilogpos", tostring(fdp))
|
||||
|
||||
if fs.access("/var/run/update_core") then
|
||||
http.write(content)
|
||||
else
|
||||
http.write(content .. "\0")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,244 @@
|
||||
require("luci.sys")
|
||||
require("luci.util")
|
||||
require("io")
|
||||
|
||||
local m, s, o, o1
|
||||
local fs = require "nixio.fs"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
|
||||
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
|
||||
local httpport = uci:get("AdGuardHome", "AdGuardHome", "httpport") or "3000"
|
||||
|
||||
local function service_running()
|
||||
if not fs.access(binpath) then
|
||||
return false
|
||||
end
|
||||
return luci.sys.call("/etc/init.d/AdGuardHome status >/dev/null 2>&1") == 0
|
||||
end
|
||||
|
||||
m = Map("AdGuardHome", "AdGuard Home")
|
||||
m.description = translate("Free and open source, powerful network-wide ads & trackers blocking DNS server.")
|
||||
|
||||
-- Inject card-style CSS for AGH pages
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Section 1: Basic Settings
|
||||
-- ============================================================
|
||||
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "基本设置")
|
||||
|
||||
o = s:option(Flag, "enabled", translate("Enable"))
|
||||
o.default = "0"
|
||||
o.optional = false
|
||||
o.template = "AdGuardHome/enable_switch"
|
||||
o.cfgvalue = function()
|
||||
return service_running() and "1" or "0"
|
||||
end
|
||||
|
||||
o = s:option(Value, "httpport", translate("Browser management port"))
|
||||
o.placeholder = "3000"
|
||||
o.default = "3000"
|
||||
o.datatype = "port"
|
||||
o.optional = false
|
||||
|
||||
o = s:option(Button, "restart", translate("Update"))
|
||||
o.inputtitle = translate("Update core version")
|
||||
o.template = "AdGuardHome/AdGuardHome_check"
|
||||
o.showfastconfig = not fs.access(configpath)
|
||||
|
||||
o = s:option(ListValue, "release_channel", translate("更新通道"),
|
||||
translate("正式版稳定;预发布版抢先体验 AdGuard 团队 beta 构建。"))
|
||||
o:value("stable", translate("正式版 (stable)"))
|
||||
o:value("beta", translate("预发布版 (beta)"))
|
||||
o.default = "stable"
|
||||
o.rmempty = false
|
||||
|
||||
local port = luci.sys.exec("awk '/^ port:/{print $2;exit}' " .. configpath .. " 2>/dev/null")
|
||||
if not port or port == "" then
|
||||
port = "?"
|
||||
end
|
||||
|
||||
o = s:option(ListValue, "redirect", port .. translate("Redirect"), translate("AdGuardHome redirect mode"))
|
||||
o:value("none", translate("不启用"))
|
||||
o:value("dnsmasq-upstream", translate("作为 dnsmasq 上游服务器"))
|
||||
o:value("redirect", translate("将 53 端口劫持到 AdGuardHome"))
|
||||
o:value("exchange", translate("使用 53 端口替代 dnsmasq"))
|
||||
o.default = "none"
|
||||
o.optional = true
|
||||
|
||||
-- ============================================================
|
||||
-- Section 2: Paths & Logs
|
||||
-- ============================================================
|
||||
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "路径与日志")
|
||||
|
||||
o = s:option(Value, "binpath", translate("Bin Path"), translate("AdGuardHome Bin path if no bin will auto download"))
|
||||
o.default = "/usr/bin/AdGuardHome"
|
||||
o.datatype = "string"
|
||||
o.optional = false
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value)
|
||||
if value == "" then return nil end
|
||||
if fs.stat(value, "type") == "dir" then
|
||||
fs.rmdir(value)
|
||||
end
|
||||
if fs.stat(value, "type") == "dir" then
|
||||
m.message = (m.message or "") .. "\n" .. translate("Error: bin path is a directory")
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "configpath", translate("Config Path"), translate("AdGuardHome config path"))
|
||||
o.default = "/etc/AdGuardHome.yaml"
|
||||
o.datatype = "string"
|
||||
o.optional = false
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value)
|
||||
if not value then return nil end
|
||||
if fs.stat(value, "type") == "dir" then
|
||||
fs.rmdir(value)
|
||||
end
|
||||
if fs.stat(value, "type") == "dir" then
|
||||
m.message = (m.message or "") .. "\n" .. translate("Error: config path is a directory")
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "workdir", translate("Work dir"), translate("AdGuardHome work dir include rules,audit log and database"))
|
||||
o.default = "/etc/AdGuardHome"
|
||||
o.datatype = "string"
|
||||
o.optional = false
|
||||
o.rmempty = false
|
||||
o.validate = function(self, value)
|
||||
if value == "" then return nil end
|
||||
if fs.stat(value, "type") == "reg" then
|
||||
m.message = (m.message or "") .. "\n" .. translate("Error: work dir is a file")
|
||||
return nil
|
||||
end
|
||||
if value:sub(-1) == "/" then
|
||||
return value:sub(1, -2)
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "logfile", translate("Runtime log file"), translate("AdGuardHome runtime Log file if 'syslog': write to system log;if empty no log"))
|
||||
o.datatype = "string"
|
||||
o.rmempty = true
|
||||
o.validate = function(self, value)
|
||||
if value and value ~= "" and fs.stat(value, "type") == "dir" then
|
||||
fs.rmdir(value)
|
||||
end
|
||||
if value and value ~= "" and fs.stat(value, "type") == "dir" then
|
||||
m.message = (m.message or "") .. "\n" .. translate("Error: log file is a directory")
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Flag, "verbose", translate("Verbose log"))
|
||||
o.default = "0"
|
||||
o.optional = true
|
||||
|
||||
-- ============================================================
|
||||
-- Section 3: Advanced Settings
|
||||
-- ============================================================
|
||||
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "高级设置")
|
||||
|
||||
o = s:option(ListValue, "upxflag", translate("use upx to compress bin after download"))
|
||||
o:value("", translate("none"))
|
||||
o:value("-1", translate("compress faster"))
|
||||
o:value("-9", translate("compress better"))
|
||||
o:value("--best", translate("compress best(can be slow for big files)"))
|
||||
o:value("--brute", translate("try all available compression methods & filters [slow]"))
|
||||
o:value("--ultra-brute", translate("try even more compression variants [very slow]"))
|
||||
o.default = ""
|
||||
o.description = translate("bin use less space,but may have compatibility issues")
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(Value, "user", translate("Service user"), translate("User the service runs under. If empty, defaults to 'adguardhome'."))
|
||||
o.placeholder = "adguardhome"
|
||||
o.datatype = "string"
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(Value, "group", translate("Service group"), translate("Group the service runs under. If empty, defaults to 'adguardhome'."))
|
||||
o.placeholder = "adguardhome"
|
||||
o.datatype = "string"
|
||||
o.rmempty = true
|
||||
|
||||
-- Helper:兼容把旧的 newline-separated 字符串读成 table(仅一次性迁移期需要)
|
||||
local function split_legacy_list(value)
|
||||
if type(value) ~= "string" or value == "" then return value end
|
||||
if not value:find("[\r\n]") then return value end
|
||||
local t = {}
|
||||
for line in value:gmatch("[^\r\n]+") do
|
||||
line = line:match("^%s*(.-)%s*$")
|
||||
if line ~= "" then t[#t+1] = line end
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
o = s:option(DynamicList, "jail_mount", translate("只读文件访问"),
|
||||
translate("AdGuardHome 可以读取的目录或文件路径,每行一项。"))
|
||||
o.placeholder = "/etc/hosts"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return split_legacy_list(uci:get("AdGuardHome", section, "jail_mount"))
|
||||
end
|
||||
|
||||
o = s:option(DynamicList, "jail_mount_rw", translate("读写文件访问"),
|
||||
translate("AdGuardHome 可以读写的目录或文件路径,每行一项。"))
|
||||
o.placeholder = "/var/log/AdGuardHome"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return split_legacy_list(uci:get("AdGuardHome", section, "jail_mount_rw"))
|
||||
end
|
||||
|
||||
-- Probe system memory to suggest a sane MiB cap (≈50% of total RAM)
|
||||
local total_mem_kb = tonumber(luci.sys.exec("awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null")) or 0
|
||||
local mem_suggest_mib = (total_mem_kb > 0) and math.floor(total_mem_kb / 1024 / 2) or 128
|
||||
|
||||
o = s:option(Value, "memlimit", translate("内存上限 (MiB)"),
|
||||
translate("达到阈值时主动触发 GC,避免被系统 OOM 杀进程。留空 = 不限制。建议设为系统内存的 50%。"))
|
||||
o.datatype = "uinteger"
|
||||
o.placeholder = translate("推荐 ") .. tostring(mem_suggest_mib)
|
||||
o.rmempty = true
|
||||
|
||||
o = s:option(Flag, "waitonboot", translate("On boot when network ok restart"))
|
||||
o.default = "1"
|
||||
o.rmempty = true
|
||||
|
||||
-- ============================================================
|
||||
-- Commit handler
|
||||
-- ============================================================
|
||||
fs.writefile("/var/run/lucilogpos", "0")
|
||||
|
||||
function m.on_commit(map)
|
||||
if fs.access("/var/run/AdGserverdis") then
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
return
|
||||
end
|
||||
|
||||
local ucitracktest = uci:get("AdGuardHome", "AdGuardHome", "ucitracktest")
|
||||
if ucitracktest == "1" then
|
||||
return
|
||||
elseif ucitracktest == "0" then
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
else
|
||||
if fs.access("/var/run/AdGlucitest") then
|
||||
uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "0")
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
else
|
||||
fs.writefile("/var/run/AdGlucitest", "")
|
||||
if ucitracktest == "2" then
|
||||
uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "1")
|
||||
else
|
||||
uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "2")
|
||||
end
|
||||
end
|
||||
uci:save("AdGuardHome")
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,16 @@
|
||||
local fs=require"nixio.fs"
|
||||
local uci=require"luci.model.uci".cursor()
|
||||
local f,t
|
||||
f=SimpleForm("logview")
|
||||
f.reset = false
|
||||
f.submit = false
|
||||
t=f:field(TextValue,"conf")
|
||||
t.rmempty=true
|
||||
t.rows=20
|
||||
t.template="AdGuardHome/log"
|
||||
t.readonly="readonly"
|
||||
local logfile=uci:get("AdGuardHome","AdGuardHome","logfile") or ""
|
||||
t.timereplace=(logfile~="syslog" and logfile~="" )
|
||||
t.pollcheck=logfile~=""
|
||||
fs.writefile("/var/run/lucilogreload","")
|
||||
return f
|
||||
@@ -0,0 +1,121 @@
|
||||
local m, s, o
|
||||
local fs = require "nixio.fs"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local sys = require "luci.sys"
|
||||
require("string")
|
||||
require("io")
|
||||
require("table")
|
||||
|
||||
m = Map("AdGuardHome")
|
||||
|
||||
|
||||
|
||||
local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
|
||||
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
|
||||
|
||||
s = m:section(TypedSection, "AdGuardHome")
|
||||
s.anonymous = true
|
||||
s.addremove = false
|
||||
|
||||
o = s:option(TextValue, "escconf")
|
||||
o.rows = 66
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
|
||||
o.cfgvalue = function(self, section)
|
||||
local tmp = fs.readfile("/tmp/AdGuardHometmpconfig.yaml")
|
||||
if tmp then return tmp end
|
||||
|
||||
local cfg = fs.readfile(configpath)
|
||||
if cfg then return cfg end
|
||||
|
||||
local dns_servers = ""
|
||||
local resolv_auto = "/tmp/resolv.conf.d/resolv.conf.auto"
|
||||
if fs.access(resolv_auto) then
|
||||
for line in io.lines(resolv_auto) do
|
||||
local ns = line:match("^[^#]*nameserver%s+([^%s]+)")
|
||||
if ns then
|
||||
dns_servers = dns_servers .. " - " .. ns .. "\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local tmpl = "/usr/share/AdGuardHome/AdGuardHome_template.yaml"
|
||||
local f = io.open(tmpl, "r")
|
||||
if not f then return "" end
|
||||
|
||||
local lines = {}
|
||||
for line in f:lines() do
|
||||
if line == "#bootstrap_dns" then
|
||||
table.insert(lines, dns_servers)
|
||||
elseif line == "#upstream_dns" then
|
||||
table.insert(lines, dns_servers)
|
||||
else
|
||||
table.insert(lines, line)
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
o.validate = function(self, value)
|
||||
if not value then return nil end
|
||||
fs.writefile("/tmp/AdGuardHometmpconfig.yaml", value:gsub("\r\n", "\n"))
|
||||
|
||||
if fs.access(binpath) then
|
||||
local ret = sys.call(binpath .. " -c /tmp/AdGuardHometmpconfig.yaml --check-config 2>/tmp/AdGuardHometest.log")
|
||||
if ret == 0 then
|
||||
return value
|
||||
end
|
||||
else
|
||||
return value
|
||||
end
|
||||
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "AdGuardHome", "manual"))
|
||||
return nil
|
||||
end
|
||||
|
||||
o.write = function(self, section, value)
|
||||
if fs.access("/tmp/AdGuardHometmpconfig.yaml") then
|
||||
fs.move("/tmp/AdGuardHometmpconfig.yaml", configpath)
|
||||
end
|
||||
end
|
||||
|
||||
o.remove = function(self, section, value)
|
||||
fs.writefile(configpath, "")
|
||||
end
|
||||
|
||||
o = s:option(DummyValue, "")
|
||||
o.anonymous = true
|
||||
o.template = "AdGuardHome/yamleditor"
|
||||
|
||||
if not fs.access(binpath) then
|
||||
o.description = translate("WARNING: no binary found, config validation will be skipped")
|
||||
end
|
||||
|
||||
if fs.access("/tmp/AdGuardHometest.log") then
|
||||
local errlog = fs.readfile("/tmp/AdGuardHometest.log")
|
||||
if errlog and errlog ~= "" then
|
||||
o = s:option(TextValue, "")
|
||||
o.readonly = true
|
||||
o.rows = 5
|
||||
o.rmempty = true
|
||||
o.name = ""
|
||||
o.cfgvalue = function(self, section)
|
||||
return fs.readfile("/tmp/AdGuardHometest.log")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function m.on_commit(map)
|
||||
local ucitracktest = uci:get("AdGuardHome", "AdGuardHome", "ucitracktest")
|
||||
if ucitracktest == "1" then
|
||||
return
|
||||
elseif ucitracktest == "0" then
|
||||
io.popen("/etc/init.d/AdGuardHome reload &")
|
||||
else
|
||||
fs.writefile("/var/run/AdGlucitest", "")
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,16 @@
|
||||
require("luci.sys")
|
||||
|
||||
local m, s
|
||||
local fs = require "nixio.fs"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
m = Map("AdGuardHome")
|
||||
|
||||
m:section(SimpleSection).template = "AdGuardHome/overview"
|
||||
|
||||
-- Read-only dashboard — hide form submit / reset buttons
|
||||
m.submit = false
|
||||
m.reset = false
|
||||
m.apply_on_parse = false
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,113 @@
|
||||
require("luci.sys")
|
||||
require("luci.util")
|
||||
|
||||
local m, s, o, o1
|
||||
local fs = require "nixio.fs"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local workdir = uci:get("AdGuardHome", "AdGuardHome", "workdir") or "/etc/AdGuardHome"
|
||||
|
||||
m = Map("AdGuardHome", translate("运维"))
|
||||
m.description = translate("改密、备份、计划任务、下载源")
|
||||
|
||||
|
||||
|
||||
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "")
|
||||
s.anonymous = true
|
||||
s.addremove = false
|
||||
|
||||
-- Change password ------------------------------------------------
|
||||
o = s:option(Value, "hashpass", translate("Change browser management password"),
|
||||
translate("Press load culculate model and culculate finally save/apply"))
|
||||
o.default = ""
|
||||
o.datatype = "string"
|
||||
o.template = "AdGuardHome/AdGuardHome_chpass"
|
||||
o.rmempty = true
|
||||
|
||||
-- Upgrade keep files ---------------------------------------------
|
||||
o = s:option(MultiValue, "upprotect", translate("Keep files when system upgrade"),
|
||||
translate("刷固件 (sysupgrade) 时保留这些文件。普通用户按推荐勾选即可:核心 + 配置 + filters + sessions.db + stats.db。日志和 querylog 通常不需要保留(querylog 可能占数十 MB)。"))
|
||||
o:value("$binpath", translate("核心执行文件 (推荐)"))
|
||||
o:value("$configpath", translate("配置文件 (必选)"))
|
||||
o:value("$logfile", translate("日志文件"))
|
||||
o:value("$workdir/data/sessions.db", translate("登录会话 sessions.db (推荐)"))
|
||||
o:value("$workdir/data/stats.db", translate("统计 stats.db (推荐)"))
|
||||
o:value("$workdir/data/querylog.json", translate("查询日志 querylog.json (占空间)"))
|
||||
o:value("$workdir/data/filters", translate("过滤规则 filters (推荐)"))
|
||||
o.widget = "checkbox"
|
||||
o.default = "$binpath $configpath $workdir/data/sessions.db $workdir/data/stats.db $workdir/data/filters"
|
||||
o.optional = false
|
||||
|
||||
-- Backup workdir -------------------------------------------------
|
||||
o = s:option(MultiValue, "backupfile", translate("Backup workdir files when shutdown"),
|
||||
translate("关机时把这些文件复制到下方「备份工作目录路径」,下次开机如果 workdir/data 为空会自动恢复。仅当工作目录位于 tmpfs(如 /tmp)时才需要勾选,持久化路径无需备份。"))
|
||||
o1 = s:option(Value, "backupwdpath", translate("Backup workdir path"))
|
||||
|
||||
o:value("filters", "filters")
|
||||
o:value("stats.db", "stats.db")
|
||||
o:value("querylog.json", "querylog.json")
|
||||
o:value("sessions.db", "sessions.db")
|
||||
o1:depends("backupfile", "filters")
|
||||
o1:depends("backupfile", "stats.db")
|
||||
o1:depends("backupfile", "querylog.json")
|
||||
o1:depends("backupfile", "sessions.db")
|
||||
|
||||
if fs.access(workdir .. "/data") then
|
||||
for name in fs.glob(workdir .. "/data/*") do
|
||||
local basename = fs.basename(name)
|
||||
if basename ~= "filters" and basename ~= "stats.db"
|
||||
and basename ~= "querylog.json" and basename ~= "sessions.db" then
|
||||
o:value(basename, basename)
|
||||
o1:depends("backupfile", basename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
o.widget = "checkbox"
|
||||
o.default = nil
|
||||
o.optional = false
|
||||
o.description = translate("Will be restore when workdir/data is empty")
|
||||
|
||||
o1.default = "/etc/AdGuardHome"
|
||||
o1.datatype = "string"
|
||||
o1.optional = false
|
||||
o1.validate = function(self, value)
|
||||
if value and value ~= "" and fs.stat(value, "type") == "reg" then
|
||||
m.message = (m.message or "") .. "\n" .. translate("Error: backup dir is a file")
|
||||
return nil
|
||||
end
|
||||
if value and value:sub(-1) == "/" then
|
||||
return value:sub(1, -2)
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
-- Crontab tasks --------------------------------------------------
|
||||
o = s:option(MultiValue, "crontab", translate("Crontab task"),
|
||||
translate("Please change time and args in crontab"))
|
||||
o:value("autoupdate", translate("Auto update core"))
|
||||
o:value("cutquerylog", translate("Auto tail querylog"))
|
||||
o:value("cutruntimelog", translate("Auto tail runtime log"))
|
||||
o:value("autohost", translate("Auto update ipv6 hosts and restart adh"))
|
||||
o.widget = "checkbox"
|
||||
o.default = nil
|
||||
o.optional = false
|
||||
|
||||
-- Download links -------------------------------------------------
|
||||
o = s:option(TextValue, "downloadlinks", translate("Download links for update"))
|
||||
o.optional = false
|
||||
o.rows = 4
|
||||
o.wrap = "soft"
|
||||
o.cfgvalue = function(self, section)
|
||||
if fs.access("/usr/share/AdGuardHome/links.txt") then
|
||||
return fs.readfile("/usr/share/AdGuardHome/links.txt")
|
||||
end
|
||||
return ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
if value then
|
||||
fs.writefile("/usr/share/AdGuardHome/links.txt", value:gsub("\r\n", "\n"))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
||||
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
<%+AdGuardHome/head%>
|
||||
<%+cbi/valueheader%>
|
||||
<script src="<%=resource%>/twin-bcrypt.min.js"></script>
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:6px;">
|
||||
<input data-update="change"<%=
|
||||
attr("id", cbid) ..
|
||||
attr("name", cbid) ..
|
||||
attr("type", self.password and "password" or "text") ..
|
||||
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
|
||||
attr("value", self:cfgvalue(section) or self.default) ..
|
||||
ifattr(self.size, "size") ..
|
||||
ifattr(self.placeholder, "placeholder") ..
|
||||
ifattr(self.readonly, "readonly") ..
|
||||
ifattr(self.maxlength, "maxlength") ..
|
||||
ifattr(self.datatype, "data-type", self.datatype) ..
|
||||
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
|
||||
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
|
||||
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
|
||||
%> />
|
||||
<% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="<%=cbid%>.applychpass" value="<%:Load culculate model%>" onclick="return chpass(this)"/>
|
||||
<input type="button" class="btn cbi-button cbi-button-save" id="<%=cbid%>.copypass" value="<%:Copy%>" style="display:none" onclick="return copypass()"/>
|
||||
</div>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var hashpassFieldId = '<%=cbid%>';
|
||||
function chpass(btn) {
|
||||
var lv = document.getElementById(hashpassFieldId);
|
||||
if (!lv || lv.value === '') { btn.value = '<%:is empty%>'; return; }
|
||||
if (typeof TwinBcrypt === 'undefined') { btn.value = '<%:loading...%>'; return; }
|
||||
btn.disabled = true;
|
||||
var hash = TwinBcrypt.hashSync(lv.value);
|
||||
lv.value = hash;
|
||||
lv.readOnly = true;
|
||||
btn.style.display = 'none';
|
||||
var copyBtn = document.getElementById(hashpassFieldId + '.copypass');
|
||||
copyBtn.style.display = '';
|
||||
copyBtn.value = '<%:Copy%>';
|
||||
}
|
||||
function copypass() {
|
||||
var lv = document.getElementById(hashpassFieldId);
|
||||
if (!lv) return;
|
||||
lv.select();
|
||||
try { document.execCommand('copy'); }
|
||||
catch(e) { navigator.clipboard.writeText(lv.value).catch(function(){}); }
|
||||
var copyBtn = document.getElementById(hashpassFieldId + '.copypass');
|
||||
copyBtn.value = '<%:Copied%>';
|
||||
setTimeout(function(){ copyBtn.value = '<%:Copy%>'; }, 2000);
|
||||
}
|
||||
//]]>
|
||||
</script>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -0,0 +1,145 @@
|
||||
<style>
|
||||
/* === AdGuardHome Status Bar === */
|
||||
.adg-status-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.adg-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.adg-status-badge::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.adg-status-running {
|
||||
background: rgba(74,160,101,.15);
|
||||
color: #2d7d46;
|
||||
}
|
||||
.adg-status-running::before {
|
||||
background: #2d7d46;
|
||||
box-shadow: 0 0 0 2px rgba(74,160,101,.3);
|
||||
}
|
||||
.adg-status-stopped {
|
||||
background: rgba(220,80,80,.12);
|
||||
color: #b03030;
|
||||
}
|
||||
.adg-status-stopped::before {
|
||||
background: #b03030;
|
||||
box-shadow: 0 0 0 2px rgba(220,80,80,.25);
|
||||
}
|
||||
.adg-status-redirect-yes {
|
||||
background: rgba(64,128,196,.12);
|
||||
color: #2864a0;
|
||||
}
|
||||
.adg-status-redirect-yes::before {
|
||||
background: #2864a0;
|
||||
box-shadow: 0 0 0 2px rgba(64,128,196,.25);
|
||||
}
|
||||
.adg-status-redirect-no {
|
||||
background: rgba(160,160,160,.10);
|
||||
color: #888;
|
||||
}
|
||||
.adg-status-redirect-no::before {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
/* === NamedSection card styling === */
|
||||
.adg-section-card {
|
||||
background: var(--cbi-background, #fff);
|
||||
border: 1px solid var(--cbi-border, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.adg-section-card .cbi-section-title {
|
||||
background: var(--cbi-section-heading-bg, #f5f7fa);
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--cbi-border, #e0e0e0);
|
||||
margin: 0;
|
||||
}
|
||||
.adg-section-card .cbi-section-title h3 {
|
||||
margin: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
/* === TextValue inputs — match text input width === */
|
||||
.cbi-value-field textarea.cbi-input-textarea {
|
||||
max-width: 280px;
|
||||
width: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* === Mobile responsive === */
|
||||
@media screen and (max-width: 600px) {
|
||||
.adg-status-bar {
|
||||
gap: 6px;
|
||||
}
|
||||
.adg-status-badge {
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
.adg-status-badge::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.adg-section-card {
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cbi-value-field .cbi-input-textarea {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* fix download links textarea on mobile */
|
||||
#cbid\\.AdGuardHome\\.AdGuardHome\\.downloadlinks {
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[status]])%>', null,
|
||||
function(x, data) {
|
||||
var tb = document.getElementById('AdGuardHome_status');
|
||||
if (data && tb) {
|
||||
var html = '<div class="adg-status-bar">';
|
||||
if (data.running) {
|
||||
html += '<span class="adg-status-badge adg-status-running">AdGuardHome <%:RUNNING%></span>';
|
||||
} else {
|
||||
html += '<span class="adg-status-badge adg-status-stopped">AdGuardHome <%:NOT RUNNING%></span>';
|
||||
}
|
||||
if (data.redirect) {
|
||||
html += '<span class="adg-status-badge adg-status-redirect-yes"><%:Redirected%></span>';
|
||||
} else {
|
||||
html += '<span class="adg-status-badge adg-status-redirect-no"><%:Not redirect%></span>';
|
||||
}
|
||||
html += '</div>';
|
||||
tb.innerHTML = html;
|
||||
}
|
||||
}
|
||||
);
|
||||
//]]>
|
||||
</script>
|
||||
<fieldset class="cbi-section">
|
||||
<div id="AdGuardHome_status">
|
||||
<div class="adg-status-bar">
|
||||
<span class="adg-status-badge adg-status-stopped"><%:Collecting data...%></span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,169 @@
|
||||
<%+AdGuardHome/head%>
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
local value = self:cfgvalue(section)
|
||||
local checked = value == "1"
|
||||
local field_id = cbid
|
||||
%>
|
||||
<style>
|
||||
.adg-enable-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 30px;
|
||||
}
|
||||
.adg-service-switch {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 26px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(128,128,128,.24);
|
||||
padding: 2px;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: background .18s ease, opacity .18s ease;
|
||||
}
|
||||
.adg-service-switch .adg-service-knob {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,.94);
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.18);
|
||||
transition: transform .18s ease, background .18s ease;
|
||||
}
|
||||
.adg-service-switch.is-on {
|
||||
background: rgba(74,160,101,.52);
|
||||
}
|
||||
.adg-service-switch.is-on .adg-service-knob {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
.adg-service-switch:disabled {
|
||||
opacity: .72;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.adg-enable-status {
|
||||
font-size: 12px;
|
||||
color: rgba(128,128,128,.86);
|
||||
}
|
||||
</style>
|
||||
<div class="adg-enable-wrap">
|
||||
<input type="hidden" data-update="change"<%=attr("id", field_id) .. attr("name", field_id) .. attr("value", checked and "1" or "0")%> />
|
||||
<button type="button" class="adg-service-switch <%=checked and 'is-on' or 'is-off'%>"<%=attr("id", field_id .. ".switch") .. attr("aria-pressed", checked and "true" or "false")%> aria-label="<%:Enable%>">
|
||||
<span class="adg-service-knob"></span>
|
||||
</button>
|
||||
<span class="adg-enable-status"<%=attr("id", field_id .. ".status")%>></span>
|
||||
</div>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
(function() {
|
||||
var hidden = document.getElementById('<%=field_id%>');
|
||||
var btn = document.getElementById('<%=field_id%>.switch');
|
||||
var status = document.getElementById('<%=field_id%>.status');
|
||||
var current = hidden && hidden.value === '1';
|
||||
var pollSerial = 0;
|
||||
|
||||
function setState(on) {
|
||||
current = !!on;
|
||||
if (hidden)
|
||||
hidden.value = current ? '1' : '0';
|
||||
if (btn) {
|
||||
btn.className = 'adg-service-switch ' + (current ? 'is-on' : 'is-off');
|
||||
btn.setAttribute('aria-pressed', current ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
function notify(message) {
|
||||
if (window.L && L.ui && L.ui.addNotification) {
|
||||
var p = document.createElement('p');
|
||||
p.appendChild(document.createTextNode(message));
|
||||
L.ui.addNotification(null, p);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(data) {
|
||||
var tb = document.getElementById('AdGuardHome_status');
|
||||
if (!data || !tb)
|
||||
return;
|
||||
|
||||
if (data.running)
|
||||
tb.innerHTML = '<em><b><font color=green>AdGuardHome <%:RUNNING%></font></b></em>';
|
||||
else
|
||||
tb.innerHTML = '<em><b><font color=red>AdGuardHome <%:NOT RUNNING%></font></b></em>';
|
||||
|
||||
if (data.redirect)
|
||||
tb.innerHTML += '<em><b><font color=green><%:Redirected%></font></b></em>';
|
||||
else
|
||||
tb.innerHTML += '<em><b><font color=red><%:Not redirect%></font></b></em>';
|
||||
}
|
||||
|
||||
function pollTarget(target, serial, left) {
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[status]])%>', null, function(_, statusData) {
|
||||
if (serial !== pollSerial)
|
||||
return;
|
||||
|
||||
updateStatus(statusData);
|
||||
if (statusData && statusData.running === target) {
|
||||
if (status)
|
||||
status.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (left <= 0) {
|
||||
setState(!target);
|
||||
if (status)
|
||||
status.innerHTML = '';
|
||||
notify(target ? '<%:AdGuardHome start failed%>' : '<%:AdGuardHome stop failed%>');
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(function() {
|
||||
pollTarget(target, serial, left - 1);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
if (!btn || !hidden)
|
||||
return;
|
||||
|
||||
btn.onclick = function(ev) {
|
||||
if (ev)
|
||||
ev.preventDefault();
|
||||
|
||||
var next = !current;
|
||||
var previous = current;
|
||||
var serial = ++pollSerial;
|
||||
setState(next);
|
||||
btn.disabled = true;
|
||||
if (status)
|
||||
status.innerHTML = next ? '<%:Starting...%>' : '<%:Stopping...%>';
|
||||
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[toggle]])%>', { enabled: next ? 1 : 0 }, function(x, data) {
|
||||
btn.disabled = false;
|
||||
|
||||
if (data && data.success) {
|
||||
setState(data.enabled);
|
||||
pollTarget(next, serial, 12);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status)
|
||||
status.innerHTML = '';
|
||||
setState(previous);
|
||||
var fallback = '<%:Operation failed%>';
|
||||
if (x && x.status && x.status !== 200)
|
||||
fallback += ' (HTTP ' + x.status + ')';
|
||||
notify(data && data.message ? data.message : fallback);
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script>
|
||||
(function () {
|
||||
// Load our CSS (CBI strips <link> tags; inject via JS)
|
||||
var link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '<%=resource%>/adguardhome/adg.css';
|
||||
document.head.appendChild(link);
|
||||
|
||||
// Add .adg-wrap to the Argon main container (exists at page load)
|
||||
var wrapEl;
|
||||
(function addWrap() {
|
||||
wrapEl = document.querySelector('.main') || document.querySelector('#maincontent') || document.body;
|
||||
wrapEl.classList.add('adg-wrap');
|
||||
})();
|
||||
|
||||
// Dark-mode detection: the LuCI theme (Argon dark / bootstrap-dark) may
|
||||
// render a dark page regardless of OS prefers-color-scheme. Sample the
|
||||
// body background and flip .adg-dark when luminance is low.
|
||||
function detectDark() {
|
||||
try {
|
||||
var bg = getComputedStyle(document.body).backgroundColor || '';
|
||||
var m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
||||
if (!m) return;
|
||||
var lum = 0.2126 * +m[1] + 0.7152 * +m[2] + 0.0722 * +m[3];
|
||||
wrapEl.classList.toggle('adg-dark', lum < 128);
|
||||
} catch (e) {}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', detectDark);
|
||||
} else {
|
||||
detectDark();
|
||||
}
|
||||
// Re-check shortly after — Argon injects dark CSS late on some pages.
|
||||
setTimeout(detectDark, 300);
|
||||
setTimeout(detectDark, 1200);
|
||||
|
||||
// Collapsible sections — wait for CBI to render (Argon loads it async)
|
||||
var COLLAPSE_KEYS = ['路径与日志', '高级设置', 'Paths & Logs', 'Advanced Settings'];
|
||||
var STORE_KEY = 'adg-collapsed';
|
||||
|
||||
function getStore() {
|
||||
try { return JSON.parse(localStorage.getItem(STORE_KEY) || '{}'); }
|
||||
catch (e) { return {}; }
|
||||
}
|
||||
function setStore(s) {
|
||||
try { localStorage.setItem(STORE_KEY, JSON.stringify(s)); } catch (e) {}
|
||||
}
|
||||
|
||||
function applyCollapse() {
|
||||
var store = getStore();
|
||||
var sections = document.querySelectorAll('.adg-wrap .cbi-section');
|
||||
if (sections.length === 0) return false;
|
||||
sections.forEach(function (sec) {
|
||||
var head = sec.querySelector(':scope > h3, :scope > legend');
|
||||
if (!head) return;
|
||||
var title = (head.textContent || '').trim();
|
||||
if (COLLAPSE_KEYS.indexOf(title) === -1) return;
|
||||
if (sec.classList.contains('adg-collapsible')) return;
|
||||
sec.classList.add('adg-collapsible');
|
||||
var collapsed = (title in store) ? store[title] : true;
|
||||
if (collapsed) sec.classList.add('adg-collapsed');
|
||||
head.addEventListener('click', function () {
|
||||
sec.classList.toggle('adg-collapsed');
|
||||
store[title] = sec.classList.contains('adg-collapsed');
|
||||
setStore(store);
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try immediately, then retry up to 20 times (10 seconds total)
|
||||
var tries = 0;
|
||||
function retry() {
|
||||
if (applyCollapse()) return;
|
||||
if (++tries < 40) setTimeout(retry, 250);
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () { setTimeout(retry, 100); });
|
||||
} else {
|
||||
setTimeout(retry, 100);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,186 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%+AdGuardHome/head%>
|
||||
<%+AdGuardHome/log_empty%>
|
||||
<style>
|
||||
.adg-log-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.adg-log-controls label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.adg-log-controls input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
}
|
||||
.adg-log-pane {
|
||||
height: 60vh;
|
||||
min-height: 360px;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(0,0,0,.08);
|
||||
border-radius: 7px;
|
||||
background: #1a1d21;
|
||||
color: #cfd6df;
|
||||
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.35 !important;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
}
|
||||
.adg-log-pane .adg-line { padding: 0 4px; border-radius: 3px; display: block; }
|
||||
.adg-log-pane .adg-info { color: #cfd6df; }
|
||||
.adg-log-pane .adg-warn { color: #e8b95a; }
|
||||
.adg-log-pane .adg-error { color: #ea7878; background: rgba(234,120,120,.06); }
|
||||
.adg-log-pane .adg-debug { color: #7a8290; opacity: .7; }
|
||||
body.dark .adg-log-pane { border-color: rgba(255,255,255,.1); }
|
||||
</style>
|
||||
<div class="adg-log-controls">
|
||||
<label><input type="checkbox" value="reverse" onclick="return reverselog()" checked><%:reverse%></input></label>
|
||||
<%if self.timereplace then%>
|
||||
<label><input type="checkbox" value="localtime" onclick="return chlogtime()" checked><%:localtime%></input></label>
|
||||
<%end%>
|
||||
</div>
|
||||
<pre id="cbid.logview.1.conf" class="adg-log-pane" style="line-height:1.35 !important"></pre>
|
||||
<div style="display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;">
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_update_button" value="<%:dellog%>" onclick="return apply_del_log()"/>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:download log%>" onclick="return download_log()"/>
|
||||
</div>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var islogreverse = true;
|
||||
var isutc2local = <%=tostring(self.timereplace)%>;
|
||||
|
||||
function hlLine(ln) {
|
||||
var cls = '';
|
||||
var l = ln.toLowerCase();
|
||||
if (l.indexOf('error') >= 0 || l.indexOf('fatal') >= 0 || l.indexOf('panic') >= 0) cls = 'adg-error';
|
||||
else if (l.indexOf('warn') >= 0) cls = 'adg-warn';
|
||||
else if (l.indexOf('debug') >= 0 || l.indexOf('trace') >= 0) cls = 'adg-debug';
|
||||
else cls = 'adg-info';
|
||||
return '<span class="adg-line ' + cls + '">' + ln.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</span>';
|
||||
}
|
||||
function hlLines(text) {
|
||||
return text.split('\n').map(hlLine).join('');
|
||||
}
|
||||
|
||||
function createAndDownloadFile(fileName, content) {
|
||||
var aTag = document.createElement('a');
|
||||
var blob = new Blob([content]);
|
||||
aTag.download = fileName;
|
||||
aTag.href = URL.createObjectURL(blob);
|
||||
aTag.click();
|
||||
URL.revokeObjectURL(blob);
|
||||
}
|
||||
function download_log(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
var dt = new Date();
|
||||
var timestamp = (dt.getMonth()+1)+"-"+dt.getDate()+"-"+dt.getHours()+"_"+dt.getMinutes();
|
||||
createAndDownloadFile("AdGuardHome"+timestamp+".log",lv.textContent)
|
||||
return
|
||||
}
|
||||
function apply_del_log(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[dodellog]])%>',null,function(x, data){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML="";
|
||||
}
|
||||
);
|
||||
return
|
||||
}
|
||||
function chlogtime(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
var text = lv.textContent;
|
||||
if (isutc2local){
|
||||
lv.innerHTML = hlLines(line_toUTC(text).join('\n'));
|
||||
isutc2local=false;
|
||||
}else{
|
||||
lv.innerHTML = hlLines(line_tolocal(text).join('\n'));
|
||||
isutc2local=true;
|
||||
}
|
||||
return
|
||||
}
|
||||
function reverselog(){
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML = lv.textContent.split('\n').reverse().map(hlLine).join('');
|
||||
if (islogreverse){
|
||||
islogreverse=false;
|
||||
}else{
|
||||
islogreverse=true;
|
||||
}
|
||||
return
|
||||
}
|
||||
function p(s) {
|
||||
return s < 10 ? '0' + s: s;
|
||||
}
|
||||
function line_tolocal(str){
|
||||
var strt=new Array();
|
||||
str.trim().split('\n').forEach(function(v, i) {
|
||||
var dt = new Date(v.substring(0,19)+" UTC");
|
||||
if (dt != "Invalid Date"){
|
||||
strt[i]=dt.getFullYear()+"/"+p(dt.getMonth()+1)+"/"+p(dt.getDate())+" "+p(dt.getHours())+":"+p(dt.getMinutes())+":"+p(dt.getSeconds())+v.substring(19);
|
||||
}else{
|
||||
strt[i]=v;}})
|
||||
return strt
|
||||
}
|
||||
function line_toUTC(str){
|
||||
var strt=new Array();
|
||||
str.trim().split('\n').forEach(function(v, i) {
|
||||
var dt = new Date(v.substring(0,19))
|
||||
if (dt != "Invalid Date"){
|
||||
strt[i]=dt.getUTCFullYear()+"/"+p(dt.getUTCMonth()+1)+"/"+p(dt.getUTCDate())+" "+p(dt.getUTCHours())+":"+p(dt.getUTCMinutes())+":"+p(dt.getUTCSeconds())+v.substring(19);
|
||||
}else{
|
||||
strt[i]=v;}})
|
||||
return strt
|
||||
}
|
||||
function displayLog(text) {
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
if (!lv) return;
|
||||
if (isutc2local) {
|
||||
var lines = line_toUTC(text);
|
||||
if (islogreverse) lines = lines.reverse();
|
||||
text = lines.join('\n');
|
||||
text = line_tolocal(text).join('\n');
|
||||
} else if (islogreverse) {
|
||||
text = text.split('\n').reverse().join('\n');
|
||||
}
|
||||
lv.innerHTML = hlLines(text);
|
||||
}
|
||||
function poll_check(){
|
||||
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[getlog]])%>', null,
|
||||
function(x, data) {
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
if (x.responseText && lv) {
|
||||
var text = x.responseText;
|
||||
if (isutc2local) {
|
||||
var lines = line_toUTC(text);
|
||||
if (islogreverse) lines = lines.reverse();
|
||||
text = lines.join('\n');
|
||||
text = line_tolocal(text).join('\n');
|
||||
} else if (islogreverse) {
|
||||
text = text.split('\n').reverse().join('\n');
|
||||
}
|
||||
if (islogreverse) {
|
||||
lv.innerHTML = hlLines(text) + lv.innerHTML;
|
||||
} else {
|
||||
lv.innerHTML += hlLines(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
);}
|
||||
<%if self.pollcheck then%>
|
||||
poll_check();
|
||||
<%else%>
|
||||
var lv = document.getElementById('cbid.logview.1.conf');
|
||||
lv.innerHTML="<%:Please add log path in config to enable log%>"
|
||||
<%end%>
|
||||
//]]>
|
||||
</script>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -0,0 +1,27 @@
|
||||
<%
|
||||
local fs = require "nixio.fs"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
|
||||
local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile") or ""
|
||||
local no_logfile = (logfile == "")
|
||||
%>
|
||||
|
||||
<% if no_logfile then %>
|
||||
<div class="adg-empty-state warn">
|
||||
<div class="adg-empty-icon">!</div>
|
||||
<div class="adg-empty-body">
|
||||
<div class="adg-empty-title"><%:运行日志未配置%></div>
|
||||
<div class="adg-empty-desc"><%:请在「基础设置 → 路径与日志 → 运行日志」填入日志路径(如 /tmp/AdGuardHome.log),或填 syslog 写入系统日志。%></div>
|
||||
<a class="btn cbi-button cbi-button-apply" href="<%=luci.dispatcher.build_url('admin','services','AdGuardHome','base')%>"><%:去设置%></a>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.adg-empty-state { display:flex; align-items:flex-start; gap:14px; padding:14px 18px; margin:6px 0 14px 0; border-radius:10px; background:rgba(128,128,128,0.05); border:1px solid rgba(128,128,128,0.18); }
|
||||
.adg-empty-state.warn { border-color:rgba(224,160,47,0.4); background:rgba(224,160,47,0.06); }
|
||||
.adg-empty-icon { font-size:22px; line-height:1; flex:0 0 auto; padding-top:2px; color:#e0a02f; }
|
||||
.adg-empty-body { flex:1; }
|
||||
.adg-empty-title { font-weight:600; font-size:14px; margin-bottom:4px; }
|
||||
.adg-empty-desc { font-size:13px; opacity:.8; margin-bottom:10px; line-height:1.55; }
|
||||
.adg-empty-state a.btn { display:inline-block; }
|
||||
</style>
|
||||
<% end %>
|
||||
@@ -0,0 +1,286 @@
|
||||
<%+AdGuardHome/head%>
|
||||
<%
|
||||
local fs = require "nixio.fs"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local sys = require "luci.sys"
|
||||
local dsp = require "luci.dispatcher"
|
||||
|
||||
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
|
||||
local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
|
||||
local httpport = uci:get("AdGuardHome", "AdGuardHome", "httpport") or "3000"
|
||||
local redirect = uci:get("AdGuardHome", "AdGuardHome", "redirect") or "none"
|
||||
local version = uci:get("AdGuardHome", "AdGuardHome", "version") or "-"
|
||||
|
||||
local has_bin = fs.access(binpath)
|
||||
local has_config = fs.access(configpath)
|
||||
|
||||
local toggle_url = dsp.build_url("admin", "services", "AdGuardHome", "toggle")
|
||||
local base_url = dsp.build_url("admin", "services", "AdGuardHome", "base")
|
||||
local tools_url = dsp.build_url("admin", "services", "AdGuardHome", "tools")
|
||||
local log_url = dsp.build_url("admin", "services", "AdGuardHome", "log")
|
||||
|
||||
local redirect_label = {
|
||||
none = translate("未启用"),
|
||||
["dnsmasq-upstream"] = translate("作为 dnsmasq 上游"),
|
||||
redirect = translate("53 端口劫持"),
|
||||
exchange = translate("替代 dnsmasq")
|
||||
}
|
||||
local redirect_text = redirect_label[redirect] or redirect
|
||||
%>
|
||||
|
||||
<style>
|
||||
/* hide LuCI form footer (Save/Apply/Reset) on this read-only dashboard */
|
||||
body:has(.adg-ov-root) .cbi-page-actions,
|
||||
body:has(.adg-ov-root) form .cbi-section + .cbi-page-actions { display: none !important; }
|
||||
|
||||
.adg-ov-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.adg-ov-card {
|
||||
background: var(--adg-card-bg, rgba(128,128,128,0.04));
|
||||
border: 1px solid var(--adg-card-border, rgba(128,128,128,0.16));
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.adg-ov-card .head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.adg-ov-card .title {
|
||||
font-size: 13px;
|
||||
color: var(--adg-label-muted, #666);
|
||||
font-weight: 500;
|
||||
}
|
||||
.adg-ov-card .value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .2px;
|
||||
}
|
||||
.adg-ov-card .sub {
|
||||
font-size: 12.5px;
|
||||
color: var(--adg-label-muted, #888);
|
||||
}
|
||||
|
||||
/* status dot */
|
||||
.adg-ov-dot {
|
||||
display: inline-block;
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: #b03030;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 0 0 3px rgba(176,48,48,.18);
|
||||
}
|
||||
.adg-ov-dot.ok { background: #2d7d46; box-shadow: 0 0 0 3px rgba(45,125,70,.18); }
|
||||
.adg-ov-dot.warn { background: #e0a02f; box-shadow: 0 0 0 3px rgba(224,160,47,.18); }
|
||||
|
||||
/* big enable switch (iOS-ish) */
|
||||
.adg-ov-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 52px; height: 30px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.adg-ov-switch input { display: none; }
|
||||
.adg-ov-switch .track {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(128,128,128,0.25);
|
||||
transition: background .2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.adg-ov-switch .knob {
|
||||
position: absolute;
|
||||
top: 3px; left: 3px;
|
||||
width: 24px; height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.2);
|
||||
transition: transform .2s;
|
||||
}
|
||||
.adg-ov-switch input:checked + .track { background: #2d7d46; }
|
||||
.adg-ov-switch input:checked + .track .knob { transform: translateX(22px); }
|
||||
.adg-ov-switch.busy .track { opacity: .6; cursor: wait; }
|
||||
|
||||
/* quick actions */
|
||||
.adg-ov-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin: 10px 0 18px 0;
|
||||
}
|
||||
.adg-ov-actions a, .adg-ov-actions button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
border: 1px solid var(--adg-card-border, rgba(128,128,128,0.18));
|
||||
background: var(--adg-card-bg, rgba(128,128,128,0.04));
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: background .15s;
|
||||
}
|
||||
.adg-ov-actions a:hover, .adg-ov-actions button:hover {
|
||||
background: rgba(0,122,255,0.08);
|
||||
border-color: rgba(0,122,255,0.4);
|
||||
}
|
||||
.adg-ov-actions a.primary {
|
||||
background: #007aff;
|
||||
border-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
.adg-ov-actions a.primary:hover { background: #0064d2; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.adg-ov-card .value { font-size: 16px; }
|
||||
.adg-ov-actions a, .adg-ov-actions button { flex: 1 1 calc(50% - 5px); justify-content: center; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="cbi-section adg-ov-root">
|
||||
<!-- Card grid -->
|
||||
<div class="adg-ov-grid">
|
||||
<!-- Status + Enable -->
|
||||
<div class="adg-ov-card">
|
||||
<div class="head">
|
||||
<div>
|
||||
<div class="title"><%:服务状态%></div>
|
||||
<div class="value" id="adg-ov-status">
|
||||
<span class="adg-ov-dot"></span><%:正在检测…%>
|
||||
</div>
|
||||
</div>
|
||||
<label class="adg-ov-switch" id="adg-ov-switch">
|
||||
<input type="checkbox" id="adg-ov-toggle">
|
||||
<span class="track"><span class="knob"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="sub" id="adg-ov-status-sub"> </div>
|
||||
</div>
|
||||
|
||||
<!-- Redirect mode -->
|
||||
<div class="adg-ov-card">
|
||||
<div class="title"><%:重定向模式%></div>
|
||||
<div class="value" id="adg-ov-redir">
|
||||
<%= redirect_text %>
|
||||
</div>
|
||||
<div class="sub" id="adg-ov-redir-sub">
|
||||
<%:配置「基础设置 → 5553 重定向」%>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core version -->
|
||||
<div class="adg-ov-card">
|
||||
<div class="title"><%:核心版本%></div>
|
||||
<div class="value">
|
||||
<% if has_bin then %>
|
||||
<%= version %>
|
||||
<% else %>
|
||||
<span style="color:#b03030;"><%:未安装%></span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="sub">
|
||||
<% if not has_config then %>
|
||||
<span style="color:#e0a02f;"><%:配置文件缺失%></span>
|
||||
<% elseif not has_bin then %>
|
||||
<%:点击「基础设置 → 更新核心版本」下载%>
|
||||
<% else %>
|
||||
<%= binpath %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick actions -->
|
||||
<div class="adg-ov-actions">
|
||||
<a class="primary" id="adg-ov-webui" target="_blank" rel="noopener"
|
||||
href="http://<%=luci.http.getenv('SERVER_NAME') or ''%>:<%=httpport%>/">
|
||||
<%:打开 Web 控制台%> :<%=httpport%>
|
||||
</a>
|
||||
<a href="<%=base_url%>"><%:基础设置%></a>
|
||||
<a href="<%=tools_url%>"><%:运维%></a>
|
||||
<a href="<%=log_url%>"><%:查看日志%></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var STATUS_URL = '<%=dsp.build_url("admin","services","AdGuardHome","status")%>';
|
||||
var TOGGLE_URL = '<%=toggle_url%>';
|
||||
|
||||
var elStatus = document.getElementById('adg-ov-status');
|
||||
var elStatusSub = document.getElementById('adg-ov-status-sub');
|
||||
var elToggle = document.getElementById('adg-ov-toggle');
|
||||
var elSwitch = document.getElementById('adg-ov-switch');
|
||||
var elRedir = document.getElementById('adg-ov-redir');
|
||||
var elRedirSub = document.getElementById('adg-ov-redir-sub');
|
||||
var elWebUI = document.getElementById('adg-ov-webui');
|
||||
|
||||
// patch the webui link to use current hostname
|
||||
if (elWebUI) {
|
||||
var host = window.location.hostname;
|
||||
elWebUI.href = 'http://' + host + ':<%=httpport%>/';
|
||||
}
|
||||
|
||||
function paint(data) {
|
||||
if (!data) return;
|
||||
// status
|
||||
if (data.running) {
|
||||
elStatus.innerHTML = '<span class="adg-ov-dot ok"></span><%:运行中%>';
|
||||
elStatusSub.textContent = '<%:AdGuardHome 正在处理 DNS 请求%>';
|
||||
elToggle.checked = true;
|
||||
} else {
|
||||
elStatus.innerHTML = '<span class="adg-ov-dot"></span><%:未运行%>';
|
||||
elStatusSub.textContent = data.message || '<%:服务未启动%>';
|
||||
elToggle.checked = false;
|
||||
}
|
||||
// redirect
|
||||
if (data.redirect) {
|
||||
elRedir.innerHTML = '<span class="adg-ov-dot ok"></span><%= redirect_text %>';
|
||||
} else {
|
||||
elRedir.innerHTML = '<span class="adg-ov-dot warn"></span><%:未重定向%>';
|
||||
}
|
||||
}
|
||||
|
||||
function poll() {
|
||||
XHR.get(STATUS_URL, null, function (x, data) { paint(data); });
|
||||
}
|
||||
|
||||
elToggle.addEventListener('change', function (ev) {
|
||||
var want = elToggle.checked ? '1' : '0';
|
||||
elSwitch.classList.add('busy');
|
||||
elToggle.disabled = true;
|
||||
XHR.post(TOGGLE_URL, { enabled: want, token: L && L.env ? L.env.token : '' }, function (x, data) {
|
||||
elSwitch.classList.remove('busy');
|
||||
elToggle.disabled = false;
|
||||
if (data) paint(data);
|
||||
poll();
|
||||
});
|
||||
});
|
||||
|
||||
// hide bottom form action bar (Save/Apply/Reset) — fallback for browsers without :has
|
||||
(function hideFooter() {
|
||||
var bar = document.querySelector('.cbi-page-actions');
|
||||
if (bar) bar.style.display = 'none';
|
||||
})();
|
||||
|
||||
// initial fetch + 3s poll
|
||||
poll();
|
||||
if (typeof XHR.poll === 'function') {
|
||||
XHR.poll(3, STATUS_URL, null, function (x, data) { paint(data); });
|
||||
} else {
|
||||
setInterval(poll, 3000);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<%+AdGuardHome/head%>
|
||||
<%+cbi/valueheader%>
|
||||
<script src="/luci-static/resources/codemirror/lib/codemirror.js"></script>
|
||||
<link rel="stylesheet" href="/luci-static/resources/codemirror/lib/codemirror.css"/>
|
||||
<style>
|
||||
.CodeMirror { line-height: 1.3; font-size: 11.5px; }
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like { line-height: 1.3; }
|
||||
</style>
|
||||
<script src="/luci-static/resources/codemirror/mode/yaml/yaml.js"></script>
|
||||
<link rel="stylesheet" href="/luci-static/resources/codemirror/theme/dracula.css"/>
|
||||
<link rel="stylesheet" href="/luci-static/resources/codemirror/addon/fold/foldgutter.css"/>
|
||||
<script src="/luci-static/resources/codemirror/addon/fold/foldcode.js"></script>
|
||||
<script src="/luci-static/resources/codemirror/addon/fold/foldgutter.js"></script>
|
||||
<script src="/luci-static/resources/codemirror/addon/fold/indent-fold.js"></script>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var editor = null;
|
||||
try {
|
||||
if (typeof CodeMirror !== 'undefined') {
|
||||
editor = CodeMirror.fromTextArea(document.getElementById("cbid.AdGuardHome.AdGuardHome.escconf"), {
|
||||
mode: "text/yaml",
|
||||
styleActiveLine: true,
|
||||
lineNumbers: true,
|
||||
theme: "dracula",
|
||||
lineWrapping: true,
|
||||
foldGutter: true,
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
matchBrackets: true
|
||||
});
|
||||
editor.setSize("100%", null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('CodeMirror init skipped:', e);
|
||||
}
|
||||
function reload_config(){
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[reloadconfig]])%>', null,
|
||||
function(x, data) {
|
||||
location.reload();
|
||||
});}
|
||||
function use_template(){
|
||||
if (!confirm("<%:即将用模板覆盖当前 YAML,未保存的修改将丢失,是否继续?%>")) return;
|
||||
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[gettemplateconfig]])%>', null,
|
||||
function(x, data) {
|
||||
if (editor) editor.setValue(x.responseText);
|
||||
else document.getElementById("cbid.AdGuardHome.AdGuardHome.escconf").value = x.responseText;
|
||||
});}
|
||||
|
||||
// Confirm before "保存并应用" / "保存" on the manual page.
|
||||
// Buttons are rendered after this script by LuCI form footer, so we
|
||||
// poll for them and wrap their inline onclick handlers.
|
||||
(function () {
|
||||
var msg = "<%:即将覆盖 AdGuardHome.yaml,请确认已备份配置。继续保存?%>";
|
||||
function wrap() {
|
||||
var targets = document.querySelectorAll('.cbi-button-save, .cbi-button-apply');
|
||||
var wrapped = 0;
|
||||
targets.forEach(function (btn) {
|
||||
if (btn.__adgWrapped) return;
|
||||
btn.__adgWrapped = true;
|
||||
wrapped++;
|
||||
var original = btn.onclick;
|
||||
btn.onclick = function (ev) {
|
||||
if (!confirm(msg)) {
|
||||
if (ev) ev.preventDefault();
|
||||
return false;
|
||||
}
|
||||
if (typeof original === 'function') return original.call(btn, ev);
|
||||
};
|
||||
});
|
||||
return wrapped;
|
||||
}
|
||||
var attempts = 0;
|
||||
var iv = setInterval(function () {
|
||||
attempts++;
|
||||
if (wrap() > 0 || attempts > 40) clearInterval(iv);
|
||||
}, 100);
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
<%fs=require"nixio.fs"%>
|
||||
<%if fs.access("/tmp/AdGuardHometmpconfig.yaml") then%>
|
||||
<input type="button" id="apply_update_button" value="<%:Reload Config%>" onclick=" return reload_config() "/>
|
||||
<%end%>
|
||||
<input type="button" id="template_button" value="<%:Use template%>" onclick=" return use_template() "/>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,231 +0,0 @@
|
||||
#/cgi-bin/luci/admin/services/AdGuardHome
|
||||
msgid "Base Setting"
|
||||
msgstr "基础设置"
|
||||
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
msgid "Basic Settings"
|
||||
msgstr "基础设置"
|
||||
|
||||
msgid "Manual Config"
|
||||
msgstr "手动设置"
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Browser management port"
|
||||
msgstr "网页管理端口"
|
||||
|
||||
msgid "DNS handling mode"
|
||||
msgstr "DNS 接管方式"
|
||||
|
||||
msgid "Do not handle DNS"
|
||||
msgstr "不接管 DNS"
|
||||
|
||||
msgid "Run as dnsmasq upstream server"
|
||||
msgstr "作为 dnsmasq 的上游服务器"
|
||||
|
||||
msgid "Forward port 53 requests through the firewall"
|
||||
msgstr "通过防火墙转发 53 端口请求"
|
||||
|
||||
msgid "Use port 53 instead of dnsmasq"
|
||||
msgstr "使用 53 端口替代 dnsmasq"
|
||||
|
||||
msgid "Network devices excluded from forwarding"
|
||||
msgstr "不参与转发的网络设备"
|
||||
|
||||
msgid "DNS requests arriving on these network devices are not forwarded to AdGuardHome"
|
||||
msgstr "从这些网络设备进入的 DNS 请求不转发到 AdGuardHome"
|
||||
|
||||
msgid "Bin Path"
|
||||
msgstr "执行文件路径"
|
||||
|
||||
msgid "Config Path"
|
||||
msgstr "配置文件路径"
|
||||
|
||||
msgid "Work dir"
|
||||
msgstr "工作目录"
|
||||
|
||||
msgid "Runtime log file"
|
||||
msgstr "运行日志文件"
|
||||
|
||||
msgid "If set to \"syslog\", write to system log; if empty, disable file logging"
|
||||
msgstr "设为 \"syslog\" 时写入系统日志;留空则禁用文件日志"
|
||||
|
||||
msgid "Verbose log"
|
||||
msgstr "详细日志"
|
||||
|
||||
msgid "Upgrade Core"
|
||||
msgstr "更新核心"
|
||||
|
||||
msgid "Update core version"
|
||||
msgstr "更新核心版本"
|
||||
|
||||
msgid "Force update"
|
||||
msgstr "强制更新"
|
||||
|
||||
msgid "Check..."
|
||||
msgstr "检查中..."
|
||||
|
||||
msgid "Updated"
|
||||
msgstr "已更新"
|
||||
|
||||
msgid "Update failed"
|
||||
msgstr "更新失败"
|
||||
|
||||
msgid "Update in progress"
|
||||
msgstr "更新正在进行中"
|
||||
|
||||
msgid "Unable to start update"
|
||||
msgstr "无法启动更新"
|
||||
|
||||
msgid "Update status unavailable"
|
||||
msgstr "更新状态不可用"
|
||||
|
||||
msgid "AdGuardHome Web"
|
||||
msgstr "AdGuardHome 仪表盘"
|
||||
|
||||
msgid "Change browser management password"
|
||||
msgstr "修改网页登录密码"
|
||||
|
||||
msgid "New password"
|
||||
msgstr "新密码"
|
||||
|
||||
msgid "Change password"
|
||||
msgstr "修改密码"
|
||||
|
||||
msgid "is empty"
|
||||
msgstr "为空"
|
||||
|
||||
msgid "Changing password..."
|
||||
msgstr "正在修改密码..."
|
||||
|
||||
msgid "Password changed"
|
||||
msgstr "密码已修改"
|
||||
|
||||
msgid "Password change failed"
|
||||
msgstr "密码修改失败"
|
||||
|
||||
msgid "Core"
|
||||
msgstr "核心"
|
||||
|
||||
msgid "core error"
|
||||
msgstr "核心错误"
|
||||
|
||||
msgid "no core"
|
||||
msgstr "没有核心"
|
||||
|
||||
msgid "no config"
|
||||
msgstr "没有配置文件"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Redirect"
|
||||
msgstr "重定向"
|
||||
|
||||
msgid "Redirected"
|
||||
msgstr "已重定向"
|
||||
|
||||
msgid "Not redirect"
|
||||
msgstr "未重定向"
|
||||
|
||||
msgid "Collecting data..."
|
||||
msgstr "获取数据中..."
|
||||
|
||||
msgid "Status unavailable"
|
||||
msgstr "状态不可用"
|
||||
|
||||
msgid "reverse"
|
||||
msgstr "逆序"
|
||||
|
||||
msgid "localtime"
|
||||
msgstr "本地时间"
|
||||
|
||||
msgid "Please add log path in config to enable log"
|
||||
msgstr "请在设置中填写日志路径以启用日志"
|
||||
|
||||
msgid "dellog"
|
||||
msgstr "删除日志"
|
||||
|
||||
msgid "download log"
|
||||
msgstr "下载日志"
|
||||
|
||||
msgid "Use template"
|
||||
msgstr "使用模板"
|
||||
|
||||
msgid "Reload Config"
|
||||
msgstr "重新载入配置"
|
||||
|
||||
msgid "Save & Apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Unable to read config"
|
||||
msgstr "无法读取配置"
|
||||
|
||||
msgid "Unable to load editor"
|
||||
msgstr "无法加载编辑器"
|
||||
|
||||
msgid "Saving..."
|
||||
msgstr "正在保存..."
|
||||
|
||||
msgid "Saved"
|
||||
msgstr "已保存"
|
||||
|
||||
msgid "Save failed"
|
||||
msgstr "保存失败"
|
||||
|
||||
msgid "Core is missing; configuration cannot be validated before saving."
|
||||
msgstr "核心文件不存在,保存前无法校验配置。"
|
||||
|
||||
msgid "Saved without validation (core missing)."
|
||||
msgstr "已保存,但因核心文件不存在而未校验。"
|
||||
|
||||
msgid "Configuration validation failed."
|
||||
msgstr "配置校验失败。"
|
||||
|
||||
msgid "Service reload failed; previous config restored."
|
||||
msgstr "服务重载失败,已恢复原配置。"
|
||||
|
||||
msgid "Restore failed; backup at"
|
||||
msgstr "恢复失败,备份文件位于"
|
||||
|
||||
msgid "Config changed on disk; reload before saving."
|
||||
msgstr "磁盘上的配置已变化,请重新读取后再保存。"
|
||||
|
||||
msgid "Unable to read runtime log"
|
||||
msgstr "无法读取运行日志"
|
||||
|
||||
msgid "Switching"
|
||||
msgstr "正在切换"
|
||||
|
||||
msgid "Checking service…"
|
||||
msgstr "正在确认服务状态…"
|
||||
|
||||
msgid "Switch not completed; check the log"
|
||||
msgstr "切换尚未完成,请检查日志"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已停用"
|
||||
|
||||
msgid "Unable to read switch status"
|
||||
msgstr "无法读取切换状态"
|
||||
|
||||
msgid "Forward port 53 requests through the firewall (recommended)"
|
||||
msgstr "通过防火墙转发 53 端口请求(推荐)"
|
||||
|
||||
msgid "Log settings"
|
||||
msgstr "日志设置"
|
||||
|
||||
msgid "Applying verbose logging…"
|
||||
msgstr "正在应用详细日志设置…"
|
||||
|
||||
msgid "Saved, but service reload failed; check the log"
|
||||
msgstr "已保存,但服务重载失败,请检查日志"
|
||||
|
||||
msgid "Unable to apply verbose logging"
|
||||
msgstr "无法应用详细日志设置"
|
||||
@@ -0,0 +1,226 @@
|
||||
bind_host: 0.0.0.0
|
||||
bind_port: 3000
|
||||
beta_bind_port: 0
|
||||
users:
|
||||
- name: admin
|
||||
password: $2y$10$vHRcARdPCieYG3RXWomV5evDYN.Nj/edtwEkQgQJZcK6z7qTLaIc6
|
||||
auth_attempts: 5
|
||||
block_auth_min: 15
|
||||
http_proxy: ""
|
||||
language: zh-cn
|
||||
rlimit_nofile: 0
|
||||
debug_pprof: false
|
||||
web_session_ttl: 720
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
port: 5553
|
||||
statistics_interval: 1
|
||||
querylog_enabled: true
|
||||
querylog_file_enabled: true
|
||||
querylog_interval: 1
|
||||
querylog_size_memory: 1000
|
||||
anonymize_client_ip: false
|
||||
protection_enabled: true
|
||||
blocking_mode: default
|
||||
blocking_ipv4: ""
|
||||
blocking_ipv6: ""
|
||||
blocked_response_ttl: 10
|
||||
parental_block_host: family-block.dns.adguard.com
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
ratelimit: 0
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: false
|
||||
upstream_dns:
|
||||
- 223.5.5.5
|
||||
upstream_dns_file: ""
|
||||
bootstrap_dns:
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
all_servers: false
|
||||
fastest_addr: false
|
||||
fastest_timeout: 1s
|
||||
allowed_clients: []
|
||||
disallowed_clients: []
|
||||
blocked_hosts:
|
||||
- version.bind
|
||||
- id.server
|
||||
- hostname.bind
|
||||
trusted_proxies:
|
||||
- 127.0.0.0/8
|
||||
- ::1/128
|
||||
cache_size: 4194304
|
||||
cache_ttl_min: 0
|
||||
cache_ttl_max: 0
|
||||
cache_optimistic: true
|
||||
bogus_nxdomain: []
|
||||
aaaa_disabled: false
|
||||
enable_dnssec: false
|
||||
edns_client_subnet: false
|
||||
max_goroutines: 300
|
||||
ipset: []
|
||||
filtering_enabled: true
|
||||
filters_update_interval: 24
|
||||
parental_enabled: false
|
||||
safesearch_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safebrowsing_cache_size: 1048576
|
||||
safesearch_cache_size: 1048576
|
||||
parental_cache_size: 1048576
|
||||
cache_time: 30
|
||||
rewrites: []
|
||||
blocked_services: []
|
||||
upstream_timeout: 10s
|
||||
local_domain_name: lan
|
||||
resolve_clients: true
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
tls:
|
||||
enabled: false
|
||||
server_name: ""
|
||||
force_https: false
|
||||
port_https: 443
|
||||
port_dns_over_tls: 853
|
||||
port_dns_over_quic: 784
|
||||
port_dnscrypt: 0
|
||||
dnscrypt_config_file: ""
|
||||
allow_unencrypted_doh: false
|
||||
strict_sni_check: false
|
||||
certificate_chain: ""
|
||||
private_key: ""
|
||||
certificate_path: ""
|
||||
private_key_path: ""
|
||||
filters:
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
|
||||
name: AdGuard DNS filter
|
||||
id: 1228750870
|
||||
- enabled: true
|
||||
url: http://sub.adtchrome.com/adt-chinalist-easylist.txt
|
||||
name: 广告终结者使用的拦截规则,基于ChinaList+EasyList修正维护
|
||||
id: 139789181
|
||||
- enabled: true
|
||||
url: https://easylist-downloads.adblockplus.org/easylist.txt
|
||||
name: EasyList-去除国际网页中大多数广告,包括不需要的框架、图像和对象
|
||||
id: 139789112
|
||||
- enabled: true
|
||||
url: https://easylist-downloads.adblockplus.org/easylistchina.txt
|
||||
name: EasyList China-EasyList针对国内的补充规则
|
||||
id: 139789121
|
||||
- enabled: true
|
||||
url: https://anti-ad.net/adguard.txt
|
||||
name: anti-AD命中率最高列表
|
||||
id: 139789122
|
||||
- enabled: true
|
||||
url: https://raw.iqiq.io/banbendalao/ADgk/master/ADgk.txt
|
||||
name: ADgk去广告easylistchina补充规则
|
||||
id: 139789131
|
||||
- enabled: true
|
||||
url: https://raw.iqiq.io/banbendalao/ADgk/master/kill-baidu-ad.txt
|
||||
name: ADgk百度广告过滤
|
||||
id: 139789133
|
||||
- enabled: false
|
||||
url: https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext
|
||||
name: Peter Lowe’s Ad and tracking server list
|
||||
id: 139789152
|
||||
- enabled: false
|
||||
url: https://easylist.to/easylist/fanboy-social.txt
|
||||
name: Fanboy's Social Blocking List
|
||||
id: 139789153
|
||||
- enabled: false
|
||||
url: https://easylist.to/easylist/fanboy-annoyance.txt
|
||||
name: Fanboy's Annoyance List
|
||||
id: 139789154
|
||||
- enabled: false
|
||||
url: https://secure.fanboy.co.nz/fanboy-cookiemonster.txt
|
||||
name: EasyList Cookie List
|
||||
id: 139789155
|
||||
- enabled: false
|
||||
url: https://fanboy.co.nz/fanboy-antifacebook.txt
|
||||
name: Anti-Facebook List
|
||||
id: 139789161
|
||||
- enabled: false
|
||||
url: https://filters.adtidy.org/extension/ublock/filters/14.txt
|
||||
name: AdGuard Annoyances filter
|
||||
id: 139789162
|
||||
- enabled: false
|
||||
url: https://www.fanboy.co.nz/enhancedstats.txt
|
||||
name: Fanboy's Enhanced Tracking List
|
||||
id: 139789164
|
||||
- enabled: false
|
||||
url: https://filters.adtidy.org/extension/ublock/filters/3.txt
|
||||
name: AdGuard Tracking Protection filter
|
||||
id: 139789165
|
||||
- enabled: false
|
||||
url: https://easylist.to/easylist/easylist.txt
|
||||
name: EasyList
|
||||
id: 139789171
|
||||
- enabled: false
|
||||
url: https://filters.adtidy.org/extension/ublock/filters/11.txt
|
||||
name: AdGuard Mobile Ads filter
|
||||
id: 139789172
|
||||
- enabled: false
|
||||
url: https://filters.adtidy.org/extension/ublock/filters/2_without_easylist.txt
|
||||
name: AdGuard Base filter
|
||||
id: 139789173
|
||||
- enabled: false
|
||||
url: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt
|
||||
name: Adblock Warning Removal List
|
||||
id: 139789174
|
||||
- enabled: false
|
||||
url: https://fanboy.co.nz/fanboy-problematic-sites.txt
|
||||
name: Fanboy's problematic-sites
|
||||
id: 139789175
|
||||
- enabled: false
|
||||
url: https://adaway.org/hosts.txt
|
||||
name: Adaway HOST
|
||||
id: 139789187
|
||||
whitelist_filters: []
|
||||
user_rules:
|
||||
- '@@||taobao.com^$important'
|
||||
- '@@||tmall.com^$important'
|
||||
- '@@||jd.com^important'
|
||||
- '@@||flyme.cn^$important'
|
||||
- '@@||meizu.com^$important'
|
||||
- '@@||wl.jd.com^$important'
|
||||
- '@@||flydigi.com^'
|
||||
- '@@||pv.sohu.com^$important'
|
||||
- /googleads.$~script,domain=~googleads.github.io
|
||||
- /pagead/lvz?
|
||||
- '||google.com/pagead/'
|
||||
- '||static.doubleclick.net^$domain=youtube.com'
|
||||
- '||youtube.com/get_midroll_'
|
||||
- '||5hz.org^'
|
||||
- '@@||000714.xyz^'
|
||||
- '@@||blueskyxn.com^'
|
||||
- '||topnewsfeeds.net^'
|
||||
- '||nbryb.com^'
|
||||
- '||superbdolly.com^'
|
||||
- '||salutationcheerlessdemote.com^'
|
||||
- '||buildingdoodlesquare.com^'
|
||||
- '||colonistnobilityheroic.com^'
|
||||
dhcp:
|
||||
enabled: false
|
||||
interface_name: ""
|
||||
dhcpv4:
|
||||
gateway_ip: ""
|
||||
subnet_mask: ""
|
||||
range_start: ""
|
||||
range_end: ""
|
||||
lease_duration: 86400
|
||||
icmp_timeout_msec: 1000
|
||||
options: []
|
||||
dhcpv6:
|
||||
range_start: ""
|
||||
lease_duration: 86400
|
||||
ra_slaac_only: false
|
||||
ra_allow_slaac: false
|
||||
clients: []
|
||||
log_compress: false
|
||||
log_localtime: false
|
||||
log_max_backups: 0
|
||||
log_max_size: 100
|
||||
log_max_age: 3
|
||||
log_file: ""
|
||||
verbose: false
|
||||
schema_version: 10
|
||||
@@ -1,11 +1,17 @@
|
||||
|
||||
config AdGuardHome 'AdGuardHome'
|
||||
option enabled '0'
|
||||
option httpport '3000'
|
||||
option redirect 'none'
|
||||
option configpath '/etc/AdGuardHome.yaml'
|
||||
option workdir '/tmp/AdGuardHome'
|
||||
option workdir '/etc/AdGuardHome'
|
||||
option logfile '/tmp/AdGuardHome.log'
|
||||
option verbose '0'
|
||||
option binpath '/usr/bin/AdGuardHome/AdGuardHome'
|
||||
list wan_ifname 'eth0'
|
||||
option binpath '/usr/bin/AdGuardHome'
|
||||
option upxflag ''
|
||||
option user ''
|
||||
option group ''
|
||||
option advanced_settings '0'
|
||||
option gc '0'
|
||||
option maxprocs '0'
|
||||
option memlimit '0'
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Executable → Regular
+6
-5
@@ -1,14 +1,15 @@
|
||||
#!/bin/sh
|
||||
chmod +x /etc/init.d/AdGuardHome
|
||||
|
||||
uci -q batch <<-EOF >/dev/null 2>&1
|
||||
delete ucitrack.@AdGuardHome[-1]
|
||||
add ucitrack AdGuardHome
|
||||
set ucitrack.@AdGuardHome[-1].init=AdGuardHome
|
||||
commit ucitrack
|
||||
delete AdGuardHome.AdGuardHome.ucitracktest
|
||||
/etc/init.d/AdGuardHome restart
|
||||
EOF
|
||||
|
||||
# Remove the former menu and both Lua and ucode menu caches.
|
||||
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
|
||||
rm -f /tmp/luci-modulecache/*
|
||||
rm -f /tmp/luci-indexcache
|
||||
|
||||
chmod +x /etc/init.d/AdGuardHome /usr/share/AdGuardHome/*
|
||||
exit 0
|
||||
@@ -1 +0,0 @@
|
||||
/etc/AdGuardHome.yaml
|
||||
Binary file not shown.
Executable → Regular
+68
-126
@@ -1,123 +1,108 @@
|
||||
# 模板用户名为 admin 密码为 admin
|
||||
# The username is admin and the password is admin.
|
||||
# 保存并应用就可以直接用的配置模板文件
|
||||
# a template config can be use with a apply
|
||||
http:
|
||||
pprof:
|
||||
port: 6060
|
||||
enabled: false
|
||||
address: 0.0.0.0:3000
|
||||
session_ttl: 720h
|
||||
bind_host: 0.0.0.0
|
||||
bind_port: 3000
|
||||
beta_bind_port: 0
|
||||
users:
|
||||
- name: admin
|
||||
password: $2y$10$iGkTW98QtGEZn4I/GC0J4eQhfuLQ53v7L4DDVauEJyWylJrXWwWeO
|
||||
- name: root
|
||||
password: $2y$10$dwn0hTYoECQMZETBErGlzOId2VANOVsPHsuH13TM/8KnysM5Dh/ve
|
||||
auth_attempts: 5
|
||||
block_auth_min: 15
|
||||
http_proxy: ""
|
||||
language: ""
|
||||
theme: auto
|
||||
language: zh-cn
|
||||
debug_pprof: false
|
||||
web_session_ttl: 720
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
port: 3053
|
||||
- 0.0.0.0
|
||||
port: 1745
|
||||
statistics_interval: 30
|
||||
querylog_enabled: true
|
||||
querylog_file_enabled: true
|
||||
querylog_interval: 6h
|
||||
querylog_size_memory: 1000
|
||||
anonymize_client_ip: false
|
||||
ratelimit: 20
|
||||
ratelimit_subnet_len_ipv4: 24
|
||||
ratelimit_subnet_len_ipv6: 56
|
||||
protection_enabled: true
|
||||
blocking_mode: default
|
||||
blocking_ipv4: ""
|
||||
blocking_ipv6: ""
|
||||
blocked_response_ttl: 60
|
||||
parental_block_host: family-block.dns.adguard.com
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
ratelimit: 0
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: true
|
||||
refuse_any: false
|
||||
upstream_dns:
|
||||
- https://dns.cloudflare.com/dns-query
|
||||
- '#'
|
||||
- '# 对 ts.net 相关域名启用 DNS 分流, 交由 Tailscale 解析, 如不使用 Tailscale 可以删除.'
|
||||
- '# Enable DNS conditional forwarding for ts.net via Tailscale; remove if Tailscale is not used.'
|
||||
- '[/ts.net/]100.100.100.100'
|
||||
- 223.5.5.5
|
||||
upstream_dns_file: ""
|
||||
bootstrap_dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
fallback_dns:
|
||||
- quic://dns.adguard-dns.com
|
||||
upstream_mode: parallel
|
||||
- 119.29.29.29
|
||||
- 223.5.5.5
|
||||
all_servers: false
|
||||
fastest_addr: false
|
||||
fastest_timeout: 1s
|
||||
allowed_clients: []
|
||||
disallowed_clients: []
|
||||
blocked_hosts:
|
||||
- version.bind
|
||||
- id.server
|
||||
- hostname.bind
|
||||
- version.bind
|
||||
- id.server
|
||||
- hostname.bind
|
||||
trusted_proxies:
|
||||
- 127.0.0.0/8
|
||||
- ::1/128
|
||||
cache_enabled: true
|
||||
- 127.0.0.0/8
|
||||
- ::1/128
|
||||
cache_size: 4194304
|
||||
cache_ttl_min: 0
|
||||
cache_ttl_max: 0
|
||||
cache_optimistic: false
|
||||
cache_optimistic_answer_ttl: 30s
|
||||
cache_optimistic_max_age: 12h
|
||||
cache_optimistic: true
|
||||
bogus_nxdomain: []
|
||||
aaaa_disabled: false
|
||||
enable_dnssec: false
|
||||
edns_client_subnet:
|
||||
custom_ip: ""
|
||||
enabled: false
|
||||
use_custom: false
|
||||
edns_client_subnet: false
|
||||
max_goroutines: 300
|
||||
handle_ddr: true
|
||||
ipset: []
|
||||
ipset_file: ""
|
||||
bootstrap_prefer_ipv6: false
|
||||
filtering_enabled: true
|
||||
filters_update_interval: 24
|
||||
parental_enabled: false
|
||||
safesearch_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
safebrowsing_cache_size: 1048576
|
||||
safesearch_cache_size: 1048576
|
||||
parental_cache_size: 1048576
|
||||
cache_time: 30
|
||||
rewrites: []
|
||||
blocked_services: []
|
||||
upstream_timeout: 10s
|
||||
private_networks: []
|
||||
local_domain_name: lan
|
||||
resolve_clients: true
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
use_dns64: false
|
||||
dns64_prefixes: []
|
||||
serve_http3: false
|
||||
use_http3_upstreams: false
|
||||
serve_plain_dns: true
|
||||
hostsfile_enabled: true
|
||||
pending_requests:
|
||||
enabled: true
|
||||
tls:
|
||||
enabled: false
|
||||
server_name: ""
|
||||
force_https: false
|
||||
port_https: 443
|
||||
port_dns_over_tls: 853
|
||||
port_dns_over_quic: 853
|
||||
port_dns_over_quic: 784
|
||||
port_dnscrypt: 0
|
||||
dnscrypt_config_file: ""
|
||||
allow_unencrypted_doh: false
|
||||
strict_sni_check: false
|
||||
certificate_chain: ""
|
||||
private_key: ""
|
||||
certificate_path: ""
|
||||
private_key_path: ""
|
||||
strict_sni_check: false
|
||||
querylog:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 2160h
|
||||
size_memory: 1000
|
||||
enabled: true
|
||||
file_enabled: true
|
||||
statistics:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 24h
|
||||
enabled: true
|
||||
filters:
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||
name: AdGuard DNS filter
|
||||
id: 1
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
|
||||
name: AdGuard DNS filter
|
||||
id: 1628750870
|
||||
- enabled: true
|
||||
url: https://anti-ad.net/easylist.txt
|
||||
name: 'CHN: anti-AD'
|
||||
id: 1628750871
|
||||
whitelist_filters: []
|
||||
user_rules: []
|
||||
dhcp:
|
||||
enabled: false
|
||||
interface_name: ""
|
||||
local_domain_name: lan
|
||||
dhcpv4:
|
||||
gateway_ip: ""
|
||||
subnet_mask: ""
|
||||
@@ -131,59 +116,16 @@ dhcp:
|
||||
lease_duration: 86400
|
||||
ra_slaac_only: false
|
||||
ra_allow_slaac: false
|
||||
filtering:
|
||||
blocking_ipv4: ""
|
||||
blocking_ipv6: ""
|
||||
blocked_services:
|
||||
schedule:
|
||||
time_zone: UTC
|
||||
ids: []
|
||||
protection_disabled_until: null
|
||||
safe_search:
|
||||
enabled: false
|
||||
bing: true
|
||||
duckduckgo: true
|
||||
ecosia: true
|
||||
google: true
|
||||
pixabay: true
|
||||
yandex: true
|
||||
youtube: true
|
||||
blocking_mode: default
|
||||
parental_block_host: family-block.dns.adguard.com
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
rewrites: []
|
||||
safe_fs_patterns:
|
||||
- /tmp/AdGuardHome/userfilters/*
|
||||
safebrowsing_cache_size: 1048576
|
||||
safesearch_cache_size: 1048576
|
||||
parental_cache_size: 1048576
|
||||
cache_time: 30
|
||||
filters_update_interval: 24
|
||||
blocked_response_ttl: 10
|
||||
filtering_enabled: true
|
||||
rewrites_enabled: true
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
protection_enabled: true
|
||||
clients:
|
||||
runtime_sources:
|
||||
whois: true
|
||||
arp: true
|
||||
rdns: true
|
||||
dhcp: true
|
||||
hosts: true
|
||||
persistent: []
|
||||
log:
|
||||
enabled: true
|
||||
file: ""
|
||||
max_backups: 0
|
||||
max_size: 100
|
||||
max_age: 3
|
||||
compress: false
|
||||
local_time: false
|
||||
verbose: false
|
||||
clients: []
|
||||
log_compress: false
|
||||
log_localtime: false
|
||||
log_max_backups: 0
|
||||
log_max_size: 100
|
||||
log_max_age: 3
|
||||
log_file: ""
|
||||
verbose: false
|
||||
os:
|
||||
group: ""
|
||||
user: ""
|
||||
rlimit_nofile: 0
|
||||
schema_version: 32
|
||||
schema_version: 12
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
|
||||
checkmd5(){
|
||||
local nowmd5
|
||||
nowmd5="$(md5sum /etc/hosts 2>/dev/null)"
|
||||
nowmd5="${nowmd5%% *}"
|
||||
local lastmd5
|
||||
lastmd5="$(uci get AdGuardHome.AdGuardHome.hostsmd5 2>/dev/null)"
|
||||
if [ "$nowmd5" != "$lastmd5" ]; then
|
||||
uci set AdGuardHome.AdGuardHome.hostsmd5="$nowmd5"
|
||||
uci commit AdGuardHome
|
||||
[ "$1" != "noreload" ] && /etc/init.d/AdGuardHome reload >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" = "del" ]; then
|
||||
sed -i '/programaddstart/,/programaddend/d' /etc/hosts
|
||||
checkmd5 "$2"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
awk 'BEGIN {
|
||||
dhcp[""] = ""
|
||||
}
|
||||
{
|
||||
if (FILENAME == "/tmp/dhcp.leases" && (NF >= 4)) {
|
||||
dhcp[$2] = $4
|
||||
}
|
||||
}
|
||||
END {
|
||||
cmd = "ip -6 neighbor show 2>/dev/null | grep -v fe80"
|
||||
while ((cmd | getline line) > 0) {
|
||||
split(line, f)
|
||||
if (f[5] in dhcp && f[5] != "") {
|
||||
print f[1] " " dhcp[f[5]]
|
||||
}
|
||||
}
|
||||
close(cmd)
|
||||
print "#programaddend"
|
||||
}' /tmp/dhcp.leases > /tmp/tmphost 2>/dev/null
|
||||
|
||||
grep -q "programaddstart" /etc/hosts 2>/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
sed -i '/programaddstart/,/programaddend/c\#programaddstart' /etc/hosts
|
||||
sed -i '/programaddstart/r /tmp/tmphost' /etc/hosts
|
||||
else
|
||||
{
|
||||
echo "#programaddstart"
|
||||
cat /tmp/tmphost
|
||||
} >> /etc/hosts
|
||||
fi
|
||||
|
||||
rm -f /tmp/tmphost
|
||||
checkmd5 "$2"
|
||||
@@ -1,10 +0,0 @@
|
||||
table inet adguardhome {
|
||||
chain prerouting {
|
||||
type nat hook prerouting priority dstnat; policy accept;
|
||||
|
||||
__AGH_WAN_RETURN_LINE__
|
||||
|
||||
fib daddr type local udp dport 53 redirect to :__AGH_PORT__
|
||||
fib daddr type local tcp dport 53 redirect to :__AGH_PORT__
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
AdGuardHome_enable=$(uci get AdGuardHome.AdGuardHome.enabled)
|
||||
redirect=$(uci get AdGuardHome.AdGuardHome.redirect)
|
||||
|
||||
if [ $AdGuardHome_enable -eq 1 -a "$redirect" == "redirect" ]; then
|
||||
/etc/init.d/AdGuardHome do_redirect 1
|
||||
fi
|
||||
@@ -4,17 +4,17 @@ PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
logread -e AdGuardHome > /tmp/AdGuardHometmp.log
|
||||
logread -e AdGuardHome -f >> /tmp/AdGuardHometmp.log &
|
||||
pid=$!
|
||||
echo "1" > /var/run/AdGuardHomesyslog
|
||||
echo "1">/var/run/AdGuardHomesyslog
|
||||
while true
|
||||
do
|
||||
sleep 12
|
||||
watchdog=$(cat /var/run/AdGuardHomesyslog 2>/dev/null)
|
||||
if [ "${watchdog}x" = "0x" ]; then
|
||||
kill "$pid"
|
||||
watchdog=$(cat /var/run/AdGuardHomesyslog)
|
||||
if [ "$watchdog"x == "0"x ]; then
|
||||
kill $pid
|
||||
rm /tmp/AdGuardHometmp.log
|
||||
rm /var/run/AdGuardHomesyslog
|
||||
exit 0
|
||||
else
|
||||
echo "0" > /var/run/AdGuardHomesyslog
|
||||
echo "0">/var/run/AdGuardHomesyslog
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1,3 @@
|
||||
https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz
|
||||
https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz
|
||||
https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
tail -n $1 "$2" > /var/run/tailtmp
|
||||
cat /var/run/tailtmp > "$2"
|
||||
rm /var/run/tailtmp
|
||||
@@ -1,164 +1,321 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
update_mode=$1
|
||||
|
||||
# Keep the lock file: unlinking it would allow a second lock on a new inode.
|
||||
exec 9>/var/lock/AdGuardHome-update.lock || exit 1
|
||||
# Allow a brief status probe to finish before treating the lock as busy.
|
||||
if ! flock -w 1 9; then
|
||||
printf '%s\n' '已经有更新任务正在运行,请等待其结束。'
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Only the lock owner may reset the log and update status.
|
||||
exec >/tmp/AdGuardHome_update.log 2>&1
|
||||
cleanup() {
|
||||
local result=$?
|
||||
trap - EXIT
|
||||
[ "$result" -ne 0 ] && touch /var/run/update_core_error
|
||||
rm -f /var/run/update_core
|
||||
exit "$result"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 1' TERM INT HUP
|
||||
rm -f /var/run/update_core_error
|
||||
touch /var/run/update_core
|
||||
binpath=$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)
|
||||
binpath="$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)"
|
||||
if [ -z "$binpath" ]; then
|
||||
uci set AdGuardHome.AdGuardHome.binpath="/tmp/AdGuardHome/AdGuardHome"
|
||||
binpath="/tmp/AdGuardHome/AdGuardHome"
|
||||
fi
|
||||
[ ! -d "${binpath%/*}" ] && mkdir -p "${binpath%/*}"
|
||||
enabled=$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
|
||||
core_api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
|
||||
|
||||
Check_Updates(){
|
||||
downloader_cmd="uclient-fetch"
|
||||
downloader_opts="-T 5 -q -O"
|
||||
downloader_quiet_opts="-q -O -"
|
||||
printf "%s\n" "开始检查更新, 请耐心等待 ..."
|
||||
Cloud_Version="$(${downloader_cmd} ${downloader_quiet_opts} "$core_api_url" 2>/dev/null | sed -n 's/.*"tag_name":[ ]*"\(v[^"]*\)".*/\1/p' | head -n 1)"
|
||||
[ -z "$Cloud_Version" ] && printf "\n检查更新失败, 请检查网络或稍后重试!\n" && EXIT 1
|
||||
if [ -f "$binpath" ]; then
|
||||
Current_Version="$($binpath --version 2>/dev/null | sed -n 's/.*\(v[0-9][0-9.]*\).*/\1/p' | head -n 1)"
|
||||
else
|
||||
Current_Version="未知"
|
||||
fi
|
||||
[ -z "$Current_Version" ] && Current_Version="未知"
|
||||
printf "\n执行文件路径: %s\n\n正在检查更新, 请耐心等待 ...\n" "${binpath%/*}"
|
||||
printf "\n当前 AdGuardHome 版本: %s\n云端 AdGuardHome 版本: %s\n" "$Current_Version" "$Cloud_Version"
|
||||
if [ "$Cloud_Version" != "$Current_Version" ] || [ "$1" = force ]; then
|
||||
Update_Core
|
||||
else
|
||||
printf "\n已是最新版本, 无需更新!\n"
|
||||
EXIT 0
|
||||
fi
|
||||
EXIT 0
|
||||
}
|
||||
mkdir -p "${binpath%/*}"
|
||||
|
||||
Update_Core(){
|
||||
rm -rf /tmp/AdGuardHome_Update > /dev/null 2>&1
|
||||
mkdir -p "/tmp/AdGuardHome_Update"
|
||||
GET_Arch
|
||||
link="https://github.com/AdguardTeam/AdGuardHome/releases/download/${Cloud_Version}/AdGuardHome_linux_${Arch}.tar.gz"
|
||||
printf "%s\n" "下载链接:${link}"
|
||||
printf "%s\n" "文件名称:${link##*/}"
|
||||
printf "\n开始下载 AdGuardHome 核心文件 ...\n\n"
|
||||
${downloader_cmd} ${downloader_opts} "/tmp/AdGuardHome_Update/${link##*/}" "$link" > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
printf "\nAdGuardHome 核心下载失败,请检查网络、系统时间及 CA 证书 ...\n"
|
||||
rm -rf /tmp/AdGuardHome_Update
|
||||
EXIT 1
|
||||
fi
|
||||
if [ "${link##*.}" = "gz" ]; then
|
||||
printf "\n正在解压 AdGuardHome ...\n"
|
||||
tar -zxf "/tmp/AdGuardHome_Update/${link##*/}" -C "/tmp/AdGuardHome_Update/"
|
||||
if [ ! -e /tmp/AdGuardHome_Update/AdGuardHome ]
|
||||
then
|
||||
printf "%s\n" "AdGuardHome 核心解压失败!"
|
||||
rm -rf "/tmp/AdGuardHome_Update" > /dev/null 2>&1
|
||||
EXIT 1
|
||||
fi
|
||||
downloadbin="/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome"
|
||||
else
|
||||
downloadbin="/tmp/AdGuardHome_Update/${link##*/}"
|
||||
fi
|
||||
chmod +x "$downloadbin"
|
||||
size_bytes=$(wc -c < "$downloadbin" 2>/dev/null || echo 0)
|
||||
printf "\nAdGuardHome 核心体积: %s\n" "$(awk -v bytes="$size_bytes" 'BEGIN{printf "%.2fMB", bytes/1000000}')"
|
||||
/etc/init.d/AdGuardHome stop 9>&- > /dev/null 2>&1
|
||||
printf "\n移动 AdGuardHome 核心文件到 %s ...\n" "${binpath%/*}"
|
||||
if ! mv -f "$downloadbin" "$binpath" > /dev/null 2>&1 || [ ! -s "$binpath" ]; then
|
||||
printf "AdGuardHome 核心移动失败!\n可能是设备空间不足导致, 请尝试更改 [执行文件路径] 为 /tmp/AdGuardHome\n"
|
||||
EXIT 1
|
||||
fi
|
||||
rm -rf /tmp/AdGuardHome_Update
|
||||
chmod +x "$binpath"
|
||||
if [ "$enabled" = 1 ]; then
|
||||
printf "\n正在重启 AdGuardHome 服务...\n"
|
||||
/etc/init.d/AdGuardHome restart 9>&- > /dev/null 2>&1
|
||||
fi
|
||||
printf "\nAdGuardHome 核心更新成功!\n"
|
||||
}
|
||||
upxflag="$(uci get AdGuardHome.AdGuardHome.upxflag 2>/dev/null)"
|
||||
|
||||
GET_Arch() {
|
||||
if command -v apk > /dev/null 2>&1; then
|
||||
PM="apk"
|
||||
elif command -v opkg > /dev/null 2>&1; then
|
||||
PM="opkg"
|
||||
else
|
||||
printf "%s\n" "未找到包管理器 (apk/opkg)!" && EXIT 1
|
||||
fi
|
||||
case "${PM}" in
|
||||
apk)
|
||||
Archt="$(apk info -a kernel 2>/dev/null | grep Architecture | awk -F ":[ ]*" '{print $2}')"
|
||||
[ -z "$Archt" ] && Archt="$(uname -m)"
|
||||
;;
|
||||
opkg)
|
||||
Archt="$(opkg info kernel | grep Architecture | awk -F "[ _]" '{print($2)}')"
|
||||
;;
|
||||
esac
|
||||
case "${Archt}" in
|
||||
i386)
|
||||
Arch=i386
|
||||
;;
|
||||
i686)
|
||||
Arch=i386
|
||||
;;
|
||||
x86|x86_64)
|
||||
Arch=amd64
|
||||
;;
|
||||
mipsel)
|
||||
Arch=mipsle_softfloat
|
||||
;;
|
||||
mips)
|
||||
Arch=mips_softfloat
|
||||
;;
|
||||
mips64el)
|
||||
Arch=mips64le_softfloat
|
||||
;;
|
||||
mips64)
|
||||
Arch=mips64_softfloat
|
||||
;;
|
||||
arm)
|
||||
Arch=arm
|
||||
;;
|
||||
armeb)
|
||||
Arch=armeb
|
||||
;;
|
||||
aarch64)
|
||||
Arch=arm64
|
||||
;;
|
||||
*)
|
||||
printf "\nAdGuardHome 暂不支持当前的设备架构: [%s]!\n" "$Archt"
|
||||
EXIT 1
|
||||
esac
|
||||
printf "\n当前设备架构: %s\n" "$Arch"
|
||||
}
|
||||
|
||||
EXIT(){
|
||||
cleanup() {
|
||||
rm -f /var/run/update_core 2>/dev/null
|
||||
[ "$1" != "0" ] && touch /var/run/update_core_error
|
||||
exit "$1"
|
||||
}
|
||||
|
||||
Check_Updates "$update_mode"
|
||||
check_already_running(){
|
||||
# 先检查锁,再 touch(顺序关键:touch 在函数调用之后执行)
|
||||
if [ -f /var/run/update_core ]; then
|
||||
echo "已有任务在运行,退出"
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
check_wgetcurl(){
|
||||
if which curl >/dev/null 2>&1; then
|
||||
downloader="curl -L -k --retry 2 --connect-timeout 20 -o"
|
||||
return 0
|
||||
fi
|
||||
if which wget-ssl >/dev/null 2>&1; then
|
||||
downloader="wget-ssl --no-check-certificate -t 2 -T 20 -O"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
opkg update >/dev/null 2>&1 || { echo "错误:opkg 更新失败"; cleanup 1; }
|
||||
fi
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
opkg remove wget wget-nossl --force-depends >/dev/null 2>&1
|
||||
opkg install wget >/dev/null 2>&1 && check_wgetcurl 1 && return 0
|
||||
fi
|
||||
|
||||
if [ "$1" = "1" ]; then
|
||||
opkg install curl >/dev/null 2>&1 && check_wgetcurl 2 && return 0
|
||||
fi
|
||||
|
||||
if [ "$1" = "2" ]; then
|
||||
check_wgetcurl && return 0
|
||||
fi
|
||||
|
||||
echo "错误:缺少 curl 和 wget"
|
||||
cleanup 1
|
||||
}
|
||||
|
||||
get_arch(){
|
||||
# 尝试多种方式获取架构:opkg → apk → uname -m
|
||||
local a
|
||||
a="$(opkg info kernel 2>/dev/null | grep Architecture | awk '{print $2}')"
|
||||
[ -z "$a" ] && a="$(apk info --architecture 2>/dev/null)"
|
||||
[ -z "$a" ] && a="$(uname -m)"
|
||||
echo "$a"
|
||||
}
|
||||
|
||||
detect_arch(){
|
||||
local Archt
|
||||
Archt="$(get_arch)"
|
||||
|
||||
case "$Archt" in
|
||||
i386|i686|i386|x86)
|
||||
Arch="386"
|
||||
;;
|
||||
x86_64|x86-64|amd64)
|
||||
Arch="amd64"
|
||||
;;
|
||||
mipsel|mipsle)
|
||||
Arch="mipsle"
|
||||
;;
|
||||
mips64el|mips64le)
|
||||
Arch="mips64le"
|
||||
;;
|
||||
mips)
|
||||
Arch="mips"
|
||||
;;
|
||||
mips64)
|
||||
Arch="mips64"
|
||||
;;
|
||||
arm|armv7*|armv8*)
|
||||
Arch="arm"
|
||||
;;
|
||||
armeb)
|
||||
Arch="armeb"
|
||||
;;
|
||||
aarch64|arm64)
|
||||
Arch="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "错误:不支持的架构:$Archt"
|
||||
cleanup 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_latest_version(){
|
||||
check_wgetcurl
|
||||
|
||||
release_channel="$(uci get AdGuardHome.AdGuardHome.release_channel 2>/dev/null)"
|
||||
[ -z "$release_channel" ] && release_channel="stable"
|
||||
|
||||
if [ "$release_channel" = "beta" ]; then
|
||||
echo "正在检查最新版本(预发布通道)..."
|
||||
# /releases 返回时间倒序的所有 release,含 prerelease。取第一条 = 最新 beta。
|
||||
local api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases"
|
||||
else
|
||||
echo "正在检查最新版本(正式版通道)..."
|
||||
local api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
|
||||
fi
|
||||
|
||||
local api_result
|
||||
api_result="$($downloader - "$api_url" 2>/dev/null)"
|
||||
latest_ver="$(echo "$api_result" | grep -oE '"tag_name": *"v[^"]+"' | head -1 | sed 's/.*"v\(.*\)".*/v\1/')"
|
||||
|
||||
if [ -z "${latest_ver}" ]; then
|
||||
echo "检查最新版本失败,请稍后重试"
|
||||
cleanup 1
|
||||
fi
|
||||
|
||||
if [ -x "$binpath" ]; then
|
||||
now_ver="$($binpath --version 2>/dev/null | grep -oE 'v[0-9.]+(-[A-Za-z0-9.]+)?' | head -1)"
|
||||
[ -z "$now_ver" ] && now_ver="$($binpath -c /dev/null --check-config 2>&1 | grep -oE 'v[0-9.]+(-[A-Za-z0-9.]+)?' | head -1)"
|
||||
else
|
||||
now_ver=""
|
||||
fi
|
||||
|
||||
echo "本地版本:${now_ver:-无},云端版本:${latest_ver}"
|
||||
|
||||
if [ "$1" = "force" ]; then
|
||||
update_core
|
||||
elif [ -z "$now_ver" ]; then
|
||||
update_core
|
||||
elif [ "$(echo "$latest_ver" | sed 's/[^0-9.]//g')" != "$(echo "$now_ver" | sed 's/[^0-9.]//g')" ]; then
|
||||
# 只有云端版本号严格大于本地时才更新
|
||||
local newer
|
||||
newer="$(printf '%s\n%s' "$latest_ver" "$now_ver" | sed 's/^v//' | sort -Vr | head -1 | sed 's/^/v/')"
|
||||
if [ "$newer" = "$latest_ver" ] && [ "$latest_ver" != "$now_ver" ]; then
|
||||
update_core
|
||||
else
|
||||
echo "已是最新版本(本地版本更高或相同)"
|
||||
cleanup 0
|
||||
fi
|
||||
else
|
||||
echo "已是最新版本"
|
||||
cleanup 0
|
||||
fi
|
||||
}
|
||||
|
||||
apply_upx(){
|
||||
[ -z "$upxflag" ] && return
|
||||
|
||||
local target="${1:-$binpath}"
|
||||
[ ! -f "$target" ] && return
|
||||
|
||||
local filesize
|
||||
filesize="$(ls -l "$target" 2>/dev/null | awk '{print $5}')"
|
||||
[ -z "$filesize" ] && return
|
||||
[ "$filesize" -le 8000000 ] && return
|
||||
|
||||
echo "二进制文件大于 8MB,正在使用 upx 压缩..."
|
||||
fetch_upx
|
||||
|
||||
local UPX_DIR="/tmp/upx-${upx_ver_nov}-${Arch}_linux"
|
||||
local UPX_BIN="${UPX_DIR}/upx"
|
||||
[ ! -x "$UPX_BIN" ] && { echo "upx 可执行文件未找到"; return; }
|
||||
|
||||
local upx_out="/tmp/AdGuardHomeupdate/upx-packed-$$"
|
||||
$UPX_BIN $upxflag "$target" -o "$upx_out"
|
||||
local upxret=$?
|
||||
rm -rf "$UPX_DIR"
|
||||
|
||||
if [ $upxret -eq 0 ]; then
|
||||
rm -f "$target"
|
||||
mv -f "$upx_out" "$target"
|
||||
chmod 755 "$target"
|
||||
echo "upx 压缩完成"
|
||||
else
|
||||
rm -f "$upx_out"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_upx(){
|
||||
local Archt_upx
|
||||
Archt_upx="$(get_arch)"
|
||||
|
||||
case "$Archt_upx" in
|
||||
i386|i686|x86|i386) Arch="i386";;
|
||||
x86_64|x86-64|amd64) Arch="amd64";;
|
||||
mipsel|mipsle) Arch="mipsel";;
|
||||
mips64el|mips64le) Arch="mipsel";;
|
||||
mips) Arch="mips";;
|
||||
mips64) Arch="mips";;
|
||||
arm|armv7*|armv8*) Arch="arm";;
|
||||
aarch64|arm64) Arch="arm64";;
|
||||
*)
|
||||
echo "upx:不支持的架构 $Archt_upx"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
upx_latest_ver="$($downloader - "https://api.github.com/repos/upx/upx/releases/latest" 2>/dev/null | grep -oE '"tag_name": *"[^"]+"' | head -1 | sed 's/.*"\(.*\)"/\1/')"
|
||||
|
||||
if [ -z "$upx_latest_ver" ]; then
|
||||
echo "获取 upx 版本失败"
|
||||
return 1
|
||||
fi
|
||||
|
||||
upx_ver_nov="$(echo "$upx_latest_ver" | sed 's/^v//')"
|
||||
local UPX_TGZ="/tmp/upx-${upx_ver_nov}-${Arch}_linux.tar.xz"
|
||||
local UPX_URL="https://github.com/upx/upx/releases/download/${upx_latest_ver}/upx-${upx_ver_nov}-${Arch}_linux.tar.xz"
|
||||
$downloader "$UPX_TGZ" "$UPX_URL" 2>&1
|
||||
[ $? -ne 0 ] && { echo "upx 下载失败"; return 1; }
|
||||
|
||||
which xz >/dev/null 2>&1 || { opkg install xz >/dev/null 2>&1 || apk add xz >/dev/null 2>&1; } || { echo "xz 不可用"; return 1; }
|
||||
|
||||
xz -d -c "$UPX_TGZ" | tar -x -C "/tmp" >/dev/null 2>&1
|
||||
|
||||
[ ! -x "/tmp/upx-${upx_ver_nov}-${Arch}_linux/upx" ] && { echo "upx 解压失败"; return 1; }
|
||||
rm -f "$UPX_TGZ"
|
||||
}
|
||||
|
||||
update_core(){
|
||||
echo "正在更新 AdGuardHome 核心..."
|
||||
mkdir -p "/tmp/AdGuardHomeupdate"
|
||||
rm -rf "/tmp/AdGuardHomeupdate/*" 2>/dev/null
|
||||
|
||||
detect_arch
|
||||
echo "架构:$Arch"
|
||||
|
||||
echo "正在获取下载链接..."
|
||||
mkdir -p /tmp/run
|
||||
# 按通道生成下载源:beta 优先 static beta;stable 优先 static release
|
||||
if [ "$release_channel" = "beta" ]; then
|
||||
{
|
||||
echo "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz"
|
||||
echo "https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz"
|
||||
} > /tmp/run/AdHlinks.txt
|
||||
else
|
||||
{
|
||||
echo "https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz"
|
||||
echo "https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz"
|
||||
} > /tmp/run/AdHlinks.txt
|
||||
fi
|
||||
|
||||
local downloadbin=""
|
||||
local success=""
|
||||
|
||||
while read link; do
|
||||
[ -z "$link" ] && continue
|
||||
eval link="$link"
|
||||
echo "尝试下载:$link"
|
||||
$downloader "/tmp/AdGuardHomeupdate/${link##*/}" "$link" 2>&1
|
||||
if [ $? -eq 0 ] && [ -s "/tmp/AdGuardHomeupdate/${link##*/}" ]; then
|
||||
downloadbin="/tmp/AdGuardHomeupdate/${link##*/}"
|
||||
success="1"
|
||||
echo "下载成功"
|
||||
break
|
||||
else
|
||||
echo "下载失败,尝试下一个..."
|
||||
rm -f "/tmp/AdGuardHomeupdate/${link##*/}"
|
||||
fi
|
||||
done < /tmp/run/AdHlinks.txt
|
||||
|
||||
rm -f /tmp/run/AdHlinks.txt
|
||||
|
||||
if [ -z "$success" ]; then
|
||||
echo "错误:所有下载源均失败"
|
||||
cleanup 1
|
||||
fi
|
||||
|
||||
if [ "${downloadbin##*.}" = "gz" ]; then
|
||||
tar -zxf "$downloadbin" -C "/tmp/AdGuardHomeupdate/" 2>/dev/null
|
||||
if [ -d "/tmp/AdGuardHomeupdate/AdGuardHome" ]; then
|
||||
downloadbin="/tmp/AdGuardHomeupdate/AdGuardHome/AdGuardHome"
|
||||
else
|
||||
echo "错误:解压失败"
|
||||
cleanup 1
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod 755 "$downloadbin"
|
||||
echo "下载完成,如已配置 upx 则进行压缩..."
|
||||
apply_upx "$downloadbin"
|
||||
|
||||
echo "正在停止服务..."
|
||||
/etc/init.d/AdGuardHome stop nobackup 2>/dev/null
|
||||
|
||||
echo "正在安装新二进制文件..."
|
||||
rm -f "$binpath" 2>/dev/null
|
||||
mv -f "$downloadbin" "$binpath" 2>/dev/null
|
||||
[ $? -ne 0 ] && { echo "mv 失败 - 磁盘空间不足?"; cleanup 1; }
|
||||
chmod 755 "$binpath"
|
||||
|
||||
rm -rf "/tmp/AdGuardHomeupdate" 2>/dev/null
|
||||
|
||||
echo "正在启动服务..."
|
||||
/etc/init.d/AdGuardHome start 2>/dev/null
|
||||
|
||||
echo "AdGuardHome 已成功更新至 ${latest_ver}"
|
||||
cleanup 0
|
||||
}
|
||||
|
||||
trap "cleanup 1" SIGTERM SIGINT
|
||||
|
||||
check_already_running
|
||||
touch /var/run/update_core
|
||||
rm -f /var/run/update_core_error 2>/dev/null
|
||||
|
||||
check_latest_version "$1"
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
count=0
|
||||
while :; do
|
||||
for host in www.baidu.com 223.5.5.5 www.google.com 8.8.8.8; do
|
||||
ping -c 1 -W 2 -q "$host" >/dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
count=$((count + 1))
|
||||
if [ $count -gt 18 ]; then
|
||||
/etc/init.d/AdGuardHome force_reload
|
||||
exit 0
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
@@ -10,4 +10,4 @@ do
|
||||
break
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
return 0
|
||||
@@ -1,45 +1,53 @@
|
||||
{
|
||||
"admin/services/AdGuardHome": {
|
||||
"title": "AdGuard Home",
|
||||
"order": 12,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/base": {
|
||||
"title": "Base Setting",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/base"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/log": {
|
||||
"title": "Log",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/log"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/manual": {
|
||||
"title": "Manual Config",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/manual"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
}
|
||||
"admin/services/AdGuardHome": {
|
||||
"title": "AdGuard Home",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "alias",
|
||||
"path": "admin/services/AdGuardHome/overview"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ]
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/overview": {
|
||||
"title": "概览",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "AdGuardHome/overview"
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/base": {
|
||||
"title": "基础设置",
|
||||
"order": 2,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "AdGuardHome/base"
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/tools": {
|
||||
"title": "运维",
|
||||
"order": 3,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "AdGuardHome/tools"
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/log": {
|
||||
"title": "日志",
|
||||
"order": 4,
|
||||
"action": {
|
||||
"type": "form",
|
||||
"path": "AdGuardHome/log"
|
||||
}
|
||||
},
|
||||
"admin/services/AdGuardHome/manual": {
|
||||
"title": "手动配置",
|
||||
"order": 5,
|
||||
"action": {
|
||||
"type": "cbi",
|
||||
"path": "AdGuardHome/manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,10 @@
|
||||
"luci-app-adguardhome": {
|
||||
"description": "Grant UCI access for luci-app-adguardhome",
|
||||
"read": {
|
||||
"file": {
|
||||
"/var/run/AdGredir": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ],
|
||||
"luci.adguardhome": [ "getCoreInfo", "getRedirectStatus", "getCoreUpdate", "getInterfaces", "getRuntimeLog", "getManualConfig" ]
|
||||
},
|
||||
"uci": [ "AdGuardHome" ]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.adguardhome": [ "setVerboseLog", "startCoreUpdate", "changeWebPassword", "clearRuntimeLog", "setManualConfig" ]
|
||||
},
|
||||
"uci": [ "AdGuardHome" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
'use strict';
|
||||
|
||||
import { stat, open, popen, readfile, writefile, unlink, lsdir } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
function shellQuote(value) {
|
||||
return "'" + replace(value, /'/g, "'\\''") + "'";
|
||||
}
|
||||
|
||||
function coreUpdateRunning() {
|
||||
// The kernel releases the lock even if the updater is killed.
|
||||
const result = system(['/usr/bin/flock', '-n', '-E', '2',
|
||||
'/var/lock/AdGuardHome-update.lock', '/bin/true'], 3000);
|
||||
if (result != 0 && result != 2)
|
||||
die('Unable to check core update lock');
|
||||
return result == 2;
|
||||
}
|
||||
|
||||
return {
|
||||
'luci.adguardhome': {
|
||||
getRedirectStatus: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
function option(name, fallback) {
|
||||
const value = conf.get('AdGuardHome', 'AdGuardHome', name);
|
||||
return (type(value) == 'array' ? value[0] : value) || fallback;
|
||||
}
|
||||
const result = {
|
||||
mode: option('redirect', 'none'),
|
||||
enabled: option('enabled', '0') == '1',
|
||||
applied_mode: option('old_redirect', 'none'),
|
||||
applied_enabled: option('old_enabled', '0') == '1',
|
||||
binpath: option('binpath', '/usr/bin/AdGuardHome/AdGuardHome'),
|
||||
redirected: trim(readfile('/var/run/AdGredir') || '') == '1'
|
||||
};
|
||||
conf.unload('AdGuardHome');
|
||||
const lock = system(['/usr/bin/flock', '-n', '-E', '2',
|
||||
'/var/lock/AdGuardHome-redirect.lock', '/bin/true'], 3000);
|
||||
if (lock != 0 && lock != 2)
|
||||
die('Unable to check redirect status');
|
||||
result.busy = lock == 2;
|
||||
return result;
|
||||
}
|
||||
},
|
||||
getManualConfig: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
conf.unload('AdGuardHome');
|
||||
|
||||
const template = readfile('/usr/share/AdGuardHome/AdGuardHome_template.yaml') || '';
|
||||
const current = readfile(configpath);
|
||||
if (current != null)
|
||||
return { content: current, template: template, source: 'current' };
|
||||
if (stat(configpath) != null)
|
||||
return { content: '', template: template, source: 'error' };
|
||||
return {
|
||||
content: template,
|
||||
template: template,
|
||||
source: template ? 'template' : 'missing'
|
||||
};
|
||||
}
|
||||
},
|
||||
setManualConfig: {
|
||||
args: { content: '' },
|
||||
call: function(request) {
|
||||
const content = request.args.content;
|
||||
if (content == null)
|
||||
return { saved: false, reason: 'invalid_content' };
|
||||
|
||||
const conf = cursor();
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
|
||||
'/usr/bin/AdGuardHome/AdGuardHome';
|
||||
conf.unload('AdGuardHome');
|
||||
|
||||
const temporaryConfig = '/tmp/AdGuardHometmpconfig.yaml';
|
||||
const validationLog = '/tmp/AdGuardHometest.log';
|
||||
if (writefile(temporaryConfig, replace(content, /\r\n/g, '\n')) == null)
|
||||
return { saved: false, reason: 'write_failed' };
|
||||
|
||||
if (stat(binpath)?.type == 'file') {
|
||||
const valid = system(['/bin/sh', '-c', '"$1" -c "$2" --check-config > "$3" 2>&1',
|
||||
'sh', binpath, temporaryConfig, validationLog], 30000) == 0;
|
||||
if (!valid)
|
||||
return { saved: false, reason: 'validation_failed', log: readfile(validationLog) || '' };
|
||||
}
|
||||
|
||||
if (writefile(configpath, readfile(temporaryConfig) || '') == null)
|
||||
return { saved: false, reason: 'save_failed' };
|
||||
unlink(temporaryConfig);
|
||||
if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
|
||||
return { saved: false, reason: 'reload_failed' };
|
||||
return { saved: true };
|
||||
}
|
||||
},
|
||||
getInterfaces: {
|
||||
call: function() {
|
||||
return { interfaces: lsdir('/sys/class/net') || [] };
|
||||
}
|
||||
},
|
||||
getCoreInfo: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
|
||||
'/usr/bin/AdGuardHome/AdGuardHome';
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
const result = {
|
||||
core_exists: stat(binpath)?.type == 'file',
|
||||
config_exists: stat(configpath)?.type == 'file',
|
||||
version: null
|
||||
};
|
||||
|
||||
if (result.core_exists) {
|
||||
// Pass the configured path as a positional argument; capture both output streams.
|
||||
const proc = popen('/bin/sh -c \'"$1" --version 2>&1\' sh ' + shellQuote(binpath), 'r');
|
||||
if (proc) {
|
||||
const output = proc.read('all') || '';
|
||||
proc.close();
|
||||
result.version = match(output, /version ([^, \t\r\n]+)/)?.[1] || null;
|
||||
}
|
||||
}
|
||||
|
||||
conf.unload('AdGuardHome');
|
||||
return result;
|
||||
}
|
||||
},
|
||||
getCoreUpdate: {
|
||||
call: function() {
|
||||
const running = coreUpdateRunning();
|
||||
const failed = stat('/var/run/update_core_error') != null ||
|
||||
(!running && stat('/var/run/update_core') != null);
|
||||
const log = readfile('/tmp/AdGuardHome_update.log', 131072) || '';
|
||||
|
||||
return {
|
||||
state: running ? 'running' : failed ? 'failure' : log ? 'finished' : 'idle',
|
||||
log: log
|
||||
};
|
||||
}
|
||||
},
|
||||
startCoreUpdate: {
|
||||
args: { force: false },
|
||||
call: function(request) {
|
||||
if (coreUpdateRunning())
|
||||
return { started: false, reason: 'busy' };
|
||||
|
||||
const command = request.args.force
|
||||
? '/bin/sh /usr/share/AdGuardHome/update_core.sh force >/dev/null 2>&1 </dev/null &'
|
||||
: '/bin/sh /usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 </dev/null &';
|
||||
|
||||
return { started: system(['/bin/sh', '-c', command], 3000) == 0 };
|
||||
}
|
||||
},
|
||||
changeWebPassword: {
|
||||
args: { hash: '' },
|
||||
call: function(request) {
|
||||
const hash = request.args.hash;
|
||||
if (!match(hash, /^\$2[aby]\$(0[4-9]|[12][0-9]|3[01])\$[.\/A-Za-z0-9]{53}$/))
|
||||
return { changed: false, reason: 'invalid_hash' };
|
||||
|
||||
const conf = cursor();
|
||||
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
|
||||
'/etc/AdGuardHome.yaml';
|
||||
if (stat(configpath)?.type != 'file')
|
||||
return { changed: false, reason: 'no_config' };
|
||||
|
||||
if (!conf.set('AdGuardHome', 'AdGuardHome', 'hashpass', hash) ||
|
||||
!conf.commit('AdGuardHome'))
|
||||
return { changed: false, reason: 'save_failed' };
|
||||
conf.unload('AdGuardHome');
|
||||
|
||||
if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
|
||||
return { changed: false, reason: 'reload_failed' };
|
||||
|
||||
if (index(readfile(configpath) || '', hash) < 0)
|
||||
return { changed: false, reason: 'verify_failed' };
|
||||
return { changed: true };
|
||||
}
|
||||
},
|
||||
setVerboseLog: {
|
||||
args: { enabled: false },
|
||||
call: function(request) {
|
||||
const conf = cursor();
|
||||
if (!conf.set('AdGuardHome', 'AdGuardHome', 'verbose', request.args.enabled ? '1' : '0') ||
|
||||
!conf.commit('AdGuardHome'))
|
||||
return { saved: false, applied: false };
|
||||
conf.unload('AdGuardHome');
|
||||
return {
|
||||
saved: true,
|
||||
applied: system(['/etc/init.d/AdGuardHome', 'reload'], 30000) == 0
|
||||
};
|
||||
}
|
||||
},
|
||||
getRuntimeLog: {
|
||||
args: { offset: 0, identity: '' },
|
||||
call: function(request) {
|
||||
const conf = cursor();
|
||||
const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
|
||||
conf.unload('AdGuardHome');
|
||||
if (!logfile)
|
||||
return { enabled: false, localtime: false, content: '', offset: 0 };
|
||||
|
||||
let source = logfile;
|
||||
if (logfile == 'syslog') {
|
||||
source = '/tmp/AdGuardHometmp.log';
|
||||
writefile('/var/run/AdGuardHomesyslog', '1');
|
||||
if (stat(source)?.type != 'file')
|
||||
system(['/bin/sh', '-c', '/bin/sh /usr/share/AdGuardHome/getsyslog.sh >/dev/null 2>&1 &'], 3000);
|
||||
}
|
||||
|
||||
// Bound memory and start with the most recent 256 KiB on a new view.
|
||||
const limit = 262144;
|
||||
const info = stat(source);
|
||||
const identity = info ? source + ':' + info.dev.major + ':' + info.dev.minor + ':' + info.inode : '';
|
||||
let offset = int(request.args.offset) || 0;
|
||||
let reset = request.args.identity != identity || offset < 0 || offset > (info?.size || 0);
|
||||
if (reset)
|
||||
offset = 0;
|
||||
if (info && info.size - offset > limit) {
|
||||
offset = info.size - limit;
|
||||
reset = true;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
if (info?.type == 'file') {
|
||||
const file = open(source, 'r');
|
||||
if (!file)
|
||||
die('Unable to open runtime log');
|
||||
const positioned = file.seek(offset);
|
||||
const chunk = positioned ? file.read(limit) : null;
|
||||
file.close();
|
||||
if (chunk == null)
|
||||
die('Unable to read runtime log');
|
||||
content = chunk;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
localtime: logfile != 'syslog',
|
||||
// ubus strings stop at NUL; cleared files can contain sparse zero-filled gaps.
|
||||
content_b64: b64enc(content),
|
||||
offset: offset + length(content),
|
||||
identity: identity,
|
||||
reset: reset
|
||||
};
|
||||
}
|
||||
},
|
||||
clearRuntimeLog: {
|
||||
call: function() {
|
||||
const conf = cursor();
|
||||
const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
|
||||
conf.unload('AdGuardHome');
|
||||
if (!logfile)
|
||||
return { cleared: false, reason: 'no_logfile' };
|
||||
|
||||
const source = logfile == 'syslog' ? '/tmp/AdGuardHometmp.log' : logfile;
|
||||
return { cleared: writefile(source, '') != null };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(n){"object"==typeof exports&&"object"==typeof module?n(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],n):n(CodeMirror)}(function(n){"use strict";function e(e,o,i,t){if(i&&i.call){var l=i;i=null}else l=r(e,i,"rangeFinder");"number"==typeof o&&(o=n.Pos(o,0));var f=r(e,i,"minFoldSize");function d(n){var r=l(e,o);if(!r||r.to.line-r.from.line<f)return null;for(var i=e.findMarksAt(r.from),d=0;d<i.length;++d)if(i[d].__isFold&&"fold"!==t){if(!n)return null;r.cleared=!0,i[d].clear()}return r}var u=d(!0);if(r(e,i,"scanUp"))for(;!u&&o.line>e.firstLine();)o=n.Pos(o.line-1,0),u=d(!1);if(u&&!u.cleared&&"unfold"!==t){var a=function(n,e){var o=r(n,e,"widget");if("string"==typeof o){var i=document.createTextNode(o);(o=document.createElement("span")).appendChild(i),o.className="CodeMirror-foldmarker"}else o&&(o=o.cloneNode(!0));return o}(e,i);n.on(a,"mousedown",function(e){c.clear(),n.e_preventDefault(e)});var c=e.markText(u.from,u.to,{replacedWith:a,clearOnEnter:r(e,i,"clearOnEnter"),__isFold:!0});c.on("clear",function(o,r){n.signal(e,"unfold",e,o,r)}),n.signal(e,"fold",e,u.from,u.to)}}n.newFoldFunction=function(n,o){return function(r,i){e(r,i,{rangeFinder:n,widget:o})}},n.defineExtension("foldCode",function(n,o,r){e(this,n,o,r)}),n.defineExtension("isFolded",function(n){for(var e=this.findMarksAt(n),o=0;o<e.length;++o)if(e[o].__isFold)return!0}),n.commands.toggleFold=function(n){n.foldCode(n.getCursor())},n.commands.fold=function(n){n.foldCode(n.getCursor(),null,"fold")},n.commands.unfold=function(n){n.foldCode(n.getCursor(),null,"unfold")},n.commands.foldAll=function(e){e.operation(function(){for(var o=e.firstLine(),r=e.lastLine();o<=r;o++)e.foldCode(n.Pos(o,0),null,"fold")})},n.commands.unfoldAll=function(e){e.operation(function(){for(var o=e.firstLine(),r=e.lastLine();o<=r;o++)e.foldCode(n.Pos(o,0),null,"unfold")})},n.registerHelper("fold","combine",function(){var n=Array.prototype.slice.call(arguments,0);return function(e,o){for(var r=0;r<n.length;++r){var i=n[r](e,o);if(i)return i}}}),n.registerHelper("fold","auto",function(n,e){for(var o=n.getHelpers(e,"fold"),r=0;r<o.length;r++){var i=o[r](n,e);if(i)return i}});var o={rangeFinder:n.fold.auto,widget:"↔",minFoldSize:0,scanUp:!1,clearOnEnter:!0};function r(n,e,r){if(e&&void 0!==e[r])return e[r];var i=n.options.foldOptions;return i&&void 0!==i[r]?i[r]:o[r]}n.defineOption("foldOptions",null),n.defineExtension("foldOption",function(n,e){return r(this,n,e)})});
|
||||
+1
@@ -0,0 +1 @@
|
||||
.CodeMirror-foldmarker{color:blue;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"\25BE"}.CodeMirror-foldgutter-folded:after{content:"\25B8"}
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("./foldcode")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./foldcode"],t):t(CodeMirror)}(function(t){"use strict";t.defineOption("foldGutter",!1,function(o,e,r){r&&r!=t.Init&&(o.clearGutter(o.state.foldGutter.options.gutter),o.state.foldGutter=null,o.off("gutterClick",a),o.off("changes",d),o.off("viewportChange",u),o.off("fold",l),o.off("unfold",l),o.off("swapDoc",d)),e&&(o.state.foldGutter=new function(t){this.options=t,this.from=this.to=0}(function(t){!0===t&&(t={});null==t.gutter&&(t.gutter="CodeMirror-foldgutter");null==t.indicatorOpen&&(t.indicatorOpen="CodeMirror-foldgutter-open");null==t.indicatorFolded&&(t.indicatorFolded="CodeMirror-foldgutter-folded");return t}(e)),f(o),o.on("gutterClick",a),o.on("changes",d),o.on("viewportChange",u),o.on("fold",l),o.on("unfold",l),o.on("swapDoc",d))});var o=t.Pos;function e(t,e){for(var r=t.findMarks(o(e,0),o(e+1,0)),n=0;n<r.length;++n)if(r[n].__isFold){var i=r[n].find(-1);if(i&&i.line===e)return r[n]}}function r(t){if("string"==typeof t){var o=document.createElement("div");return o.className=t+" CodeMirror-guttermarker-subtle",o}return t.cloneNode(!0)}function n(t,n,f){var a=t.state.foldGutter.options,d=n-1,u=t.foldOption(a,"minFoldSize"),l=t.foldOption(a,"rangeFinder"),c="string"==typeof a.indicatorFolded&&i(a.indicatorFolded),s="string"==typeof a.indicatorOpen&&i(a.indicatorOpen);t.eachLine(n,f,function(n){++d;var i=null,f=n.gutterMarkers;if(f&&(f=f[a.gutter]),e(t,d)){if(c&&f&&c.test(f.className))return;i=r(a.indicatorFolded)}else{var p=o(d,0),m=l&&l(t,p);if(m&&m.to.line-m.from.line>=u){if(s&&f&&s.test(f.className))return;i=r(a.indicatorOpen)}}(i||f)&&t.setGutterMarker(n,a.gutter,i)})}function i(t){return new RegExp("(^|\\s)"+t+"(?:$|\\s)\\s*")}function f(t){var o=t.getViewport(),e=t.state.foldGutter;e&&(t.operation(function(){n(t,o.from,o.to)}),e.from=o.from,e.to=o.to)}function a(t,r,n){var i=t.state.foldGutter;if(i){var f=i.options;if(n==f.gutter){var a=e(t,r);a?a.clear():t.foldCode(o(r,0),f)}}}function d(t){var o=t.state.foldGutter;if(o){var e=o.options;o.from=o.to=0,clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){f(t)},e.foldOnChangeTimeSpan||600)}}function u(t){var o=t.state.foldGutter;if(o){var e=o.options;clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){var e=t.getViewport();o.from==o.to||e.from-o.to>20||o.from-e.to>20?f(t):t.operation(function(){e.from<o.from&&(n(t,e.from,o.from),o.from=e.from),e.to>o.to&&(n(t,o.to,e.to),o.to=e.to)})},e.updateViewportTimeSpan||400)}}function l(t,o){var e=t.state.foldGutter;if(e){var r=o.line;r>=e.from&&r<e.to&&n(t,r,r+1)}}});
|
||||
+1
@@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";function n(n,t){var i=n.getLine(t),o=i.search(/\S/);return-1==o||/\bcomment\b/.test(n.getTokenTypeAt(e.Pos(t,o+1)))?-1:e.countColumn(i,null,n.getOption("tabSize"))}e.registerHelper("fold","indent",function(t,i){var o=n(t,i.line);if(!(o<0)){for(var r=null,l=i.line+1,f=t.lastLine();l<=f;++l){var u=n(t,l);if(-1==u);else{if(!(u>o))break;r=l}}return r?{from:e.Pos(i.line,t.getLine(i.line).length),to:e.Pos(r,t.getLine(r).length)}:void 0}})});
|
||||
@@ -1,276 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require view/adguardhome/status as status';
|
||||
|
||||
var callInterfaces = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'getInterfaces',
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callCoreUpdate = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'getCoreUpdate',
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callStartCoreUpdate = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'startCoreUpdate',
|
||||
params: [ 'force' ],
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
return view.extend({
|
||||
handleSaveApply: function(ev, mode) {
|
||||
var target = {
|
||||
mode: this.redirectOption.formvalue('AdGuardHome') || 'none',
|
||||
enabled: this.enabledOption.formvalue('AdGuardHome') === '1'
|
||||
};
|
||||
this.sharedStatus.beginSwitch(target);
|
||||
return this.handleSave(ev).then(function() {
|
||||
ui.changes.apply(mode == '0');
|
||||
}).catch(function(error) {
|
||||
this.sharedStatus.cancelSwitch();
|
||||
throw error;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
load: function() {
|
||||
return callInterfaces().catch(function() { return { interfaces: [] }; });
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
let m, s, o;
|
||||
var self = this;
|
||||
var interfaces = data.interfaces || [];
|
||||
|
||||
m = new form.Map('AdGuardHome');
|
||||
|
||||
s = m.section(
|
||||
form.TypedSection,
|
||||
'AdGuardHome',
|
||||
_('Basic Settings')
|
||||
);
|
||||
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(
|
||||
form.Flag,
|
||||
'enabled',
|
||||
_('Enable')
|
||||
);
|
||||
this.enabledOption = o;
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(
|
||||
form.Value,
|
||||
'httpport',
|
||||
_('Browser management port')
|
||||
);
|
||||
o.datatype = 'port';
|
||||
o.placeholder = '3000';
|
||||
o.default = '3000';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(
|
||||
form.ListValue,
|
||||
'redirect',
|
||||
_('DNS handling mode')
|
||||
);
|
||||
|
||||
this.redirectOption = o;
|
||||
o.value('none', _('Do not handle DNS'));
|
||||
o.value('redirect', _('Forward port 53 requests through the firewall (recommended)'));
|
||||
o.value('dnsmasq-upstream', _('Run as dnsmasq upstream server'));
|
||||
o.value('exchange', _('Use port 53 instead of dnsmasq'));
|
||||
|
||||
o.default = 'none';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(
|
||||
form.DynamicList,
|
||||
'wan_ifname',
|
||||
_('Network devices excluded from forwarding'),
|
||||
_('DNS requests arriving on these network devices are not forwarded to AdGuardHome')
|
||||
);
|
||||
o.depends('redirect', 'redirect');
|
||||
o.retain = true;
|
||||
o.datatype = 'string';
|
||||
o.rmempty = true;
|
||||
interfaces.filter(function(name) { return name !== 'lo'; }).forEach(function(name) {
|
||||
o.value(name, name);
|
||||
});
|
||||
|
||||
o = s.option(
|
||||
form.Value,
|
||||
'binpath',
|
||||
_('Bin Path')
|
||||
);
|
||||
o.default = '/usr/bin/AdGuardHome/AdGuardHome';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(
|
||||
form.Value,
|
||||
'configpath',
|
||||
_('Config Path')
|
||||
);
|
||||
o.default = '/etc/AdGuardHome.yaml';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(
|
||||
form.Value,
|
||||
'workdir',
|
||||
_('Work dir')
|
||||
);
|
||||
o.default = '/usr/bin/AdGuardHome';
|
||||
o.rmempty = false;
|
||||
|
||||
|
||||
return m.render().then(function(node) {
|
||||
var sharedStatus = status.render();
|
||||
self.sharedStatus = sharedStatus;
|
||||
|
||||
var updateButton = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
|
||||
}, _('Update core version'));
|
||||
var forceButton = E('button', {
|
||||
'class': 'btn cbi-button', 'type': 'button'
|
||||
}, _('Force update'));
|
||||
var updateMessage = E('span', {}, '');
|
||||
var reverseLog = E('input', { 'type': 'checkbox' });
|
||||
var updateLog = E('textarea', {
|
||||
'class': 'cbi-input-textarea agh-update-log', 'rows': 8, 'readonly': 'readonly',
|
||||
'style': 'display:none;'
|
||||
});
|
||||
var logOptions = E('label', { 'class': 'agh-update-options', 'style': 'display:none;' }, [
|
||||
reverseLog, E('span', {}, _('reverse'))
|
||||
]);
|
||||
var updatePanel = E('div', { 'class': 'cbi-value' }, [
|
||||
E('div', { 'class': 'cbi-value-title' }, _('Upgrade Core')),
|
||||
E('div', { 'class': 'cbi-value-field' }, E('div', { 'class': 'agh-update-content' }, [
|
||||
E('div', { 'class': 'agh-toolbar' }, [updateButton, forceButton, updateMessage]),
|
||||
logOptions, updateLog
|
||||
]))
|
||||
]);
|
||||
var lastLog = '';
|
||||
var startingAt = 0;
|
||||
var activeUpdate = false;
|
||||
var updateFinishedHere = false;
|
||||
|
||||
function showLog() {
|
||||
updateLog.value = reverseLog.checked ? lastLog.split('\n').reverse().join('\n') : lastLog;
|
||||
}
|
||||
reverseLog.addEventListener('change', showLog);
|
||||
|
||||
function showUpdateStatus(result) {
|
||||
var state = result.state || 'idle';
|
||||
if (startingAt && state === 'idle' && Date.now() - startingAt < 10000)
|
||||
return;
|
||||
startingAt = 0;
|
||||
if (state === 'running')
|
||||
activeUpdate = true;
|
||||
else if (activeUpdate && (state === 'finished' || state === 'failure')) {
|
||||
activeUpdate = false;
|
||||
updateFinishedHere = true;
|
||||
sharedStatus.refresh();
|
||||
}
|
||||
|
||||
updateButton.disabled = forceButton.disabled = state === 'running';
|
||||
if (state === 'running' || updateFinishedHere) {
|
||||
updateMessage.textContent = state === 'running' ? _('Check...') :
|
||||
state === 'finished' ? _('Updated') : _('Update failed');
|
||||
lastLog = result.log || '';
|
||||
logOptions.style.display = lastLog ? 'inline-flex' : 'none';
|
||||
updateLog.style.display = lastLog ? 'block' : 'none';
|
||||
showLog();
|
||||
} else {
|
||||
updateMessage.textContent = '';
|
||||
lastLog = '';
|
||||
logOptions.style.display = updateLog.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function startUpdate(force) {
|
||||
startingAt = Date.now();
|
||||
activeUpdate = true;
|
||||
updateFinishedHere = false;
|
||||
updateButton.disabled = forceButton.disabled = true;
|
||||
updateMessage.textContent = _('Check...');
|
||||
lastLog = '';
|
||||
showLog();
|
||||
updateLog.style.display = logOptions.style.display = 'none';
|
||||
callStartCoreUpdate(force).then(function(result) {
|
||||
if (!result.started) {
|
||||
startingAt = 0;
|
||||
activeUpdate = false;
|
||||
updateMessage.textContent = _('Update in progress');
|
||||
return;
|
||||
}
|
||||
return callCoreUpdate().then(showUpdateStatus);
|
||||
}).catch(function() {
|
||||
startingAt = 0;
|
||||
activeUpdate = false;
|
||||
updateButton.disabled = forceButton.disabled = false;
|
||||
updateMessage.textContent = _('Unable to start update');
|
||||
});
|
||||
}
|
||||
|
||||
updateButton.addEventListener('click', function() { startUpdate(false); });
|
||||
forceButton.addEventListener('click', function() { startUpdate(true); });
|
||||
poll.add(function() {
|
||||
return callCoreUpdate().then(showUpdateStatus).catch(function() {
|
||||
updateMessage.textContent = _('Update status unavailable');
|
||||
});
|
||||
}, 3);
|
||||
var settingsCard = node.querySelector('.cbi-section');
|
||||
settingsCard.classList.add('agh-compact-card', 'agh-base-card');
|
||||
var settingsFields = settingsCard.querySelector('.cbi-section-node') || settingsCard;
|
||||
settingsFields.appendChild(updatePanel);
|
||||
node.insertBefore(sharedStatus.node, node.firstChild);
|
||||
node.insertBefore(E('link', {
|
||||
'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css')
|
||||
}), node.firstChild);
|
||||
|
||||
var portInput = node.querySelector('[data-name="httpport"] input');
|
||||
var portField = node.querySelector('[data-name="httpport"] .cbi-value-field');
|
||||
if (portInput && portField) {
|
||||
var webLink = E('input', {
|
||||
'type': 'button',
|
||||
'style': 'width:210px;border-color:Teal;text-align:center;font-weight:bold;color:#337ab7;background:#ffc800;',
|
||||
'target': '_blank',
|
||||
'value': _('AdGuardHome Web')
|
||||
});
|
||||
var linkWrap = E('div', { 'style': 'margin-top:0.35rem;' }, webLink);
|
||||
var host = window.location.hostname;
|
||||
if (host.indexOf(':') >= 0 && host.charAt(0) !== '[')
|
||||
host = '[' + host + ']';
|
||||
|
||||
function updateWebLink() {
|
||||
var port = portInput.value.trim();
|
||||
var valid = /^[0-9]+$/.test(port) && +port >= 1 && +port <= 65535;
|
||||
webLink.value = _('AdGuardHome Web') + ': ' + port;
|
||||
webLink.onclick = valid ? function() {
|
||||
window.open('http://' + host + ':' + port + '/');
|
||||
} : null;
|
||||
webLink.style.display = valid ? 'inline-block' : 'none';
|
||||
}
|
||||
|
||||
portField.appendChild(linkWrap);
|
||||
portInput.addEventListener('input', updateWebLink);
|
||||
updateWebLink();
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,153 +0,0 @@
|
||||
/* Scoped to the settings and log cards; shared status badges keep their layout. */
|
||||
.agh-compact-card {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.agh-compact-card .cbi-value {
|
||||
margin-bottom: 0;
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
.agh-compact-card .cbi-value-description {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.agh-compact-card .agh-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 0.75rem;
|
||||
}
|
||||
.agh-compact-card .agh-toolbar > * {
|
||||
margin: 0;
|
||||
}
|
||||
.agh-compact-card .agh-update-content {
|
||||
width: 100%;
|
||||
max-width: 40rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.agh-compact-card .agh-update-options {
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
width: auto;
|
||||
margin: 0.5rem 0 0;
|
||||
padding: 0;
|
||||
float: none;
|
||||
}
|
||||
.agh-compact-card .agh-update-options input[type="checkbox"] {
|
||||
position: static;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
}
|
||||
.agh-compact-card textarea.agh-update-log {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
min-height: 6rem;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
margin: 0.35rem 0 0;
|
||||
}
|
||||
.agh-log-view {
|
||||
padding: 0.6em;
|
||||
}
|
||||
.agh-log-view textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: clamp(12rem, 32vh, 20rem);
|
||||
min-height: 10rem;
|
||||
box-sizing: border-box;
|
||||
resize: vertical;
|
||||
margin: 0 0 0.6em;
|
||||
}
|
||||
|
||||
.agh-manual-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
.agh-compact-card .agh-password-controls label {
|
||||
float: none;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
.agh-compact-card .agh-password-controls input {
|
||||
width: 14rem;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agh-manual-card {
|
||||
padding: 0.6em;
|
||||
}
|
||||
.agh-editor-wrap {
|
||||
margin: 0 0 0.6em;
|
||||
}
|
||||
.agh-config-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Keep the base card heading in the theme's original position. */
|
||||
.agh-base-card .cbi-section-node {
|
||||
margin: 0;
|
||||
padding: 0.6em;
|
||||
}
|
||||
.agh-log-view .agh-log-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6em 1.2em;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.agh-log-view .agh-log-toggles {
|
||||
justify-content: flex-start;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.agh-log-view .agh-log-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.6em 1.2em;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
.agh-log-view .agh-log-actions > .cbi-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex: 0 1 auto;
|
||||
gap: 0.5em;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.agh-log-view .agh-log-actions .cbi-value-title {
|
||||
float: none;
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.agh-log-view .agh-log-actions .cbi-value-field {
|
||||
flex: 0 1 auto;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.agh-log-view .agh-log-actions .cbi-value-field input {
|
||||
width: 22em;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.agh-log-controls .agh-toolbar > span:empty {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require view/adguardhome/status as status';
|
||||
|
||||
var callRuntimeLog = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'getRuntimeLog',
|
||||
params: [ 'offset', 'identity' ],
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callClearRuntimeLog = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'clearRuntimeLog',
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callSetVerboseLog = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'setVerboseLog',
|
||||
params: [ 'enabled' ],
|
||||
expect: { '': {} },
|
||||
reject: true,
|
||||
timeout: 45000
|
||||
});
|
||||
|
||||
function decodeLog(result) {
|
||||
var content = result.content || '';
|
||||
if (typeof result.content_b64 === 'string') {
|
||||
var binary = atob(result.content_b64);
|
||||
var bytes = new Uint8Array(binary.length);
|
||||
for (var i = 0; i < binary.length; i++)
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
content = new TextDecoder('utf-8').decode(bytes);
|
||||
}
|
||||
// Truncating an open log does not reset a non-append writer's file position.
|
||||
return content.replace(/\0/g, '');
|
||||
}
|
||||
|
||||
function pad(value) {
|
||||
return value < 10 ? '0' + value : value;
|
||||
}
|
||||
|
||||
function localizeTimestamps(content) {
|
||||
return content.split('\n').map(function(line) {
|
||||
var date = new Date(line.substring(0, 19).replace(/\//g, '-') + ' UTC');
|
||||
if (isNaN(date.getTime()))
|
||||
return line;
|
||||
|
||||
return date.getFullYear() + '/' + pad(date.getMonth() + 1) + '/' + pad(date.getDate()) + ' ' +
|
||||
pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()) + line.substring(19);
|
||||
}).join('\n');
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return callRuntimeLog(0, '').catch(function() {
|
||||
return { error: true, localtime: false, content: '', offset: 0 };
|
||||
});
|
||||
},
|
||||
|
||||
render: function(initialLog) {
|
||||
var m = new form.Map('AdGuardHome');
|
||||
var s = m.section(form.TypedSection, 'AdGuardHome');
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
var o;
|
||||
|
||||
o = s.option(
|
||||
form.Value,
|
||||
'logfile',
|
||||
_('Runtime log file')
|
||||
);
|
||||
o.rmempty = true;
|
||||
|
||||
return m.render().then(function(settingsNode) {
|
||||
var sharedStatus = status.render();
|
||||
var logLimit = 262144;
|
||||
var content = decodeLog(initialLog).slice(-logLimit);
|
||||
var identity = initialLog.identity || '';
|
||||
var generation = 0;
|
||||
var offset = initialLog.offset || 0;
|
||||
var enabled = initialLog.enabled;
|
||||
var localtime = initialLog.localtime;
|
||||
var reverse = true;
|
||||
var convertTime = localtime;
|
||||
var logArea = E('textarea', {
|
||||
'class': 'cbi-input-textarea',
|
||||
'rows': 16,
|
||||
'readonly': 'readonly',
|
||||
'style': 'width:100%;box-sizing:border-box;'
|
||||
});
|
||||
var toggleStyle = 'margin:0 0.35em 0 0;height:auto;flex:none;';
|
||||
var reverseToggle = E('input', { 'type': 'checkbox', 'checked': true, 'style': toggleStyle });
|
||||
var timeToggle = E('input', { 'type': 'checkbox', 'checked': true, 'style': toggleStyle });
|
||||
var reverseLabel = E('label', { 'style': 'display:inline-flex;align-items:center;' }, [
|
||||
reverseToggle, _('reverse')
|
||||
]);
|
||||
var timeLabel = E('label', { 'style': localtime ? 'display:inline-flex;align-items:center;' : 'display:none;' }, [
|
||||
timeToggle, _('localtime')
|
||||
]);
|
||||
var verboseToggle = E('input', {
|
||||
'type': 'checkbox', 'style': toggleStyle
|
||||
});
|
||||
// Boolean HTML attributes are true whenever present, even with a false value.
|
||||
verboseToggle.checked = m.data.get('AdGuardHome', 'AdGuardHome', 'verbose') === '1';
|
||||
verboseToggle.disabled = !!m.readonly;
|
||||
var verboseMessage = E('span', { 'role': 'status' }, '');
|
||||
var verboseLabel = E('label', { 'style': 'display:inline-flex;align-items:center;' }, [
|
||||
verboseToggle, _('Verbose log')
|
||||
]);
|
||||
verboseToggle.addEventListener('change', function() {
|
||||
var requested = verboseToggle.checked;
|
||||
verboseToggle.disabled = true;
|
||||
verboseMessage.textContent = _('Applying verbose logging…');
|
||||
callSetVerboseLog(requested).then(function(result) {
|
||||
if (!result.saved)
|
||||
throw new Error('save failed');
|
||||
verboseMessage.textContent = result.applied ? '' : _('Saved, but service reload failed; check the log');
|
||||
}).catch(function() {
|
||||
verboseToggle.checked = !requested;
|
||||
verboseMessage.textContent = _('Unable to apply verbose logging');
|
||||
}).then(function() {
|
||||
verboseToggle.disabled = false;
|
||||
});
|
||||
});
|
||||
var message = E('span', {}, '');
|
||||
|
||||
function renderLog() {
|
||||
var displayed = convertTime ? localizeTimestamps(content) : content;
|
||||
logArea.value = reverse ? displayed.split('\n').reverse().join('\n') : displayed;
|
||||
}
|
||||
|
||||
function downloadLog() {
|
||||
var now = new Date();
|
||||
var link = document.createElement('a');
|
||||
link.download = 'AdGuardHome' + (now.getMonth() + 1) + '-' + now.getDate() + '-' +
|
||||
pad(now.getHours()) + '_' + pad(now.getMinutes()) + '.log';
|
||||
link.href = URL.createObjectURL(new Blob([logArea.value], { type: 'text/plain' }));
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
var clearButton = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
|
||||
}, _('dellog'));
|
||||
var downloadButton = E('button', {
|
||||
'class': 'btn cbi-button', 'type': 'button'
|
||||
}, _('download log'));
|
||||
reverseToggle.addEventListener('change', function() {
|
||||
reverse = reverseToggle.checked;
|
||||
renderLog();
|
||||
});
|
||||
timeToggle.addEventListener('change', function() {
|
||||
convertTime = timeToggle.checked;
|
||||
renderLog();
|
||||
});
|
||||
clearButton.addEventListener('click', function() {
|
||||
clearButton.disabled = true;
|
||||
generation++;
|
||||
callClearRuntimeLog().then(function(result) {
|
||||
if (!result.cleared)
|
||||
throw new Error(result.reason || 'clear failed');
|
||||
content = '';
|
||||
offset = 0;
|
||||
identity = '';
|
||||
generation++;
|
||||
message.textContent = '';
|
||||
renderLog();
|
||||
}).catch(function() {
|
||||
message.textContent = _('Status unavailable');
|
||||
}).then(function() {
|
||||
clearButton.disabled = false;
|
||||
});
|
||||
});
|
||||
downloadButton.addEventListener('click', downloadLog);
|
||||
|
||||
var settingsCard = settingsNode.querySelector('.cbi-section');
|
||||
settingsCard.classList.add('agh-compact-card', 'agh-log-view');
|
||||
var settingsFields = settingsCard.querySelector('.cbi-section-node');
|
||||
var logFileField = settingsFields.querySelector('.cbi-value');
|
||||
var logPath = logFileField.querySelector('input[type="text"]');
|
||||
if (logPath)
|
||||
logPath.title = _('If set to "syslog", write to system log; if empty, disable file logging');
|
||||
var logControls = E('div', { 'class': 'agh-log-controls' }, [
|
||||
E('div', { 'class': 'agh-toolbar agh-log-toggles' }, [
|
||||
reverseLabel, timeLabel, verboseLabel, verboseMessage, message
|
||||
]),
|
||||
E('div', { 'class': 'agh-log-actions' }, [
|
||||
E('div', { 'class': 'agh-toolbar' }, [clearButton, downloadButton]),
|
||||
logFileField
|
||||
])
|
||||
]);
|
||||
// Keep the form field inside its map, outside the theme's section layout.
|
||||
settingsFields.hidden = true;
|
||||
settingsCard.insertBefore(logControls, settingsFields);
|
||||
settingsCard.insertBefore(logArea, logControls);
|
||||
var page = E('div', {}, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css') }),
|
||||
sharedStatus.node,
|
||||
settingsNode
|
||||
]);
|
||||
|
||||
if (initialLog.error)
|
||||
message.textContent = _('Unable to read runtime log');
|
||||
else if (!enabled)
|
||||
message.textContent = _('Please add log path in config to enable log');
|
||||
renderLog();
|
||||
|
||||
poll.add(function() {
|
||||
if (clearButton.disabled)
|
||||
return Promise.resolve();
|
||||
var requestGeneration = generation;
|
||||
return callRuntimeLog(offset, identity).then(function(result) {
|
||||
if (requestGeneration !== generation)
|
||||
return;
|
||||
enabled = result.enabled;
|
||||
if (!enabled) {
|
||||
message.textContent = _('Please add log path in config to enable log');
|
||||
return;
|
||||
}
|
||||
message.textContent = '';
|
||||
if (result.reset || result.offset < offset)
|
||||
content = '';
|
||||
offset = result.offset;
|
||||
identity = result.identity || '';
|
||||
content = (content + decodeLog(result)).slice(-logLimit);
|
||||
renderLog();
|
||||
}).catch(function() {
|
||||
message.textContent = _('Status unavailable');
|
||||
});
|
||||
}, 3);
|
||||
|
||||
return page;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,232 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require view/adguardhome/status as status';
|
||||
|
||||
var callManualConfig = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'getManualConfig',
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callSetManualConfig = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'setManualConfig',
|
||||
params: [ 'content' ],
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callChangeWebPassword = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'changeWebPassword',
|
||||
params: [ 'hash' ],
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var codeMirrorLoader;
|
||||
var bcryptLoader;
|
||||
|
||||
function loadBcrypt() {
|
||||
if (window.TwinBcrypt)
|
||||
return Promise.resolve(window.TwinBcrypt);
|
||||
|
||||
if (!bcryptLoader) {
|
||||
bcryptLoader = new Promise(function(resolve, reject) {
|
||||
var script = document.createElement('script');
|
||||
script.src = L.resource('twin-bcrypt.min.js');
|
||||
script.onload = function() {
|
||||
if (window.TwinBcrypt)
|
||||
resolve(window.TwinBcrypt);
|
||||
else
|
||||
reject(new Error('bcrypt unavailable'));
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
}).catch(function(error) {
|
||||
bcryptLoader = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return bcryptLoader;
|
||||
}
|
||||
|
||||
function loadCodeMirror() {
|
||||
if (window.CodeMirror)
|
||||
return Promise.resolve(window.CodeMirror);
|
||||
if (!codeMirrorLoader) {
|
||||
codeMirrorLoader = new Promise(function(resolve, reject) {
|
||||
var stylesheet = document.createElement('link');
|
||||
stylesheet.rel = 'stylesheet';
|
||||
stylesheet.href = L.resource('codemirror/lib/codemirror.css');
|
||||
document.head.appendChild(stylesheet);
|
||||
var theme = document.createElement('link');
|
||||
theme.rel = 'stylesheet';
|
||||
theme.href = L.resource('codemirror/theme/dracula.css');
|
||||
document.head.appendChild(theme);
|
||||
var script = document.createElement('script');
|
||||
script.src = L.resource('codemirror/lib/codemirror.js');
|
||||
script.onload = function() {
|
||||
var yaml = document.createElement('script');
|
||||
yaml.src = L.resource('codemirror/mode/yaml/yaml.js');
|
||||
yaml.onload = function() { resolve(window.CodeMirror); };
|
||||
yaml.onerror = reject;
|
||||
document.head.appendChild(yaml);
|
||||
};
|
||||
script.onerror = reject;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
return codeMirrorLoader;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return callManualConfig().catch(function() {
|
||||
return { content: '', template: '', source: 'error' };
|
||||
});
|
||||
},
|
||||
|
||||
render: function(initial) {
|
||||
var sharedStatus = status.render();
|
||||
var config = initial;
|
||||
var showingTemplate = false;
|
||||
var editor;
|
||||
var message = E('span', { 'style': 'margin-left:0.8em;' }, '');
|
||||
var configArea = E('textarea', {
|
||||
'class': 'cbi-input-textarea',
|
||||
'rows': 20,
|
||||
'wrap': 'off',
|
||||
'style': 'width:100%;box-sizing:border-box;font-family:monospace;'
|
||||
});
|
||||
var editorWrap = E('div', { 'class': 'agh-editor-wrap' }, configArea);
|
||||
var templateButton = E('button', {
|
||||
'class': 'btn cbi-button', 'type': 'button'
|
||||
}, _('Use template'));
|
||||
var reloadButton = E('button', {
|
||||
'class': 'btn cbi-button', 'type': 'button', 'style': 'margin-left:0.6em;'
|
||||
}, _('Reload Config'));
|
||||
var saveButton = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply', 'type': 'button', 'style': 'margin-left:0.6em;'
|
||||
}, _('Save & Apply'));
|
||||
var passwordInput = E('input', {
|
||||
'class': 'cbi-input-password', 'type': 'password',
|
||||
'id': 'agh-web-password', 'autocomplete': 'new-password', 'spellcheck': 'false'
|
||||
});
|
||||
var passwordButton = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
|
||||
}, _('Change password'));
|
||||
var passwordMessage = E('span', { 'style': 'margin-left:0.8em;' }, '');
|
||||
var passwordPanel = E('div', { 'class': 'agh-toolbar agh-password-controls' }, [
|
||||
E('label', { 'for': 'agh-web-password' }, _('Change browser management password')),
|
||||
passwordInput, passwordButton, passwordMessage
|
||||
]);
|
||||
|
||||
function showConfig() {
|
||||
var content = showingTemplate ? config.template : config.content;
|
||||
if (editor)
|
||||
editor.setValue(content);
|
||||
else
|
||||
configArea.value = content;
|
||||
templateButton.disabled = !config.template;
|
||||
}
|
||||
|
||||
templateButton.addEventListener('click', function() {
|
||||
showingTemplate = true;
|
||||
message.textContent = '';
|
||||
showConfig();
|
||||
});
|
||||
reloadButton.addEventListener('click', function() {
|
||||
reloadButton.disabled = true;
|
||||
message.textContent = '';
|
||||
callManualConfig().then(function(result) {
|
||||
config = result;
|
||||
showingTemplate = false;
|
||||
showConfig();
|
||||
}).catch(function() {
|
||||
message.textContent = _('Unable to read config');
|
||||
}).then(function() {
|
||||
reloadButton.disabled = false;
|
||||
});
|
||||
});
|
||||
saveButton.addEventListener('click', function() {
|
||||
var content = editor ? editor.getValue() : configArea.value;
|
||||
saveButton.disabled = true;
|
||||
message.textContent = _('Saving...');
|
||||
callSetManualConfig(content).then(function(result) {
|
||||
if (!result.saved)
|
||||
throw new Error(result.log || result.reason || 'save failed');
|
||||
config.content = content;
|
||||
config.source = 'current';
|
||||
showingTemplate = false;
|
||||
message.textContent = _('Saved');
|
||||
}).catch(function(error) {
|
||||
message.textContent = error.message || _('Save failed');
|
||||
}).then(function() {
|
||||
saveButton.disabled = false;
|
||||
});
|
||||
});
|
||||
passwordButton.addEventListener('click', function() {
|
||||
var password = passwordInput.value;
|
||||
if (!password) {
|
||||
passwordMessage.textContent = _('New password') + ' ' + _('is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
passwordButton.disabled = true;
|
||||
passwordMessage.textContent = _('Changing password...');
|
||||
loadBcrypt().then(function(bcrypt) {
|
||||
var hash = bcrypt.hashSync(password);
|
||||
password = null;
|
||||
passwordInput.value = '';
|
||||
return callChangeWebPassword(hash);
|
||||
}).then(function(result) {
|
||||
if (!result.changed)
|
||||
throw new Error(result.reason || 'change failed');
|
||||
passwordMessage.textContent = _('Password changed');
|
||||
}).catch(function() {
|
||||
passwordMessage.textContent = _('Password change failed');
|
||||
}).then(function() {
|
||||
passwordButton.disabled = false;
|
||||
});
|
||||
});
|
||||
showConfig();
|
||||
|
||||
var page = E('div', {}, [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css') }),
|
||||
sharedStatus.node,
|
||||
E('div', { 'class': 'cbi-section agh-compact-card agh-manual-card' }, [
|
||||
editorWrap,
|
||||
E('div', { 'class': 'agh-manual-toolbar' }, [
|
||||
passwordPanel,
|
||||
E('div', { 'class': 'agh-toolbar agh-config-actions' }, [message, templateButton, reloadButton, saveButton])
|
||||
])
|
||||
])
|
||||
]);
|
||||
loadCodeMirror().then(function(CodeMirror) {
|
||||
editor = CodeMirror.fromTextArea(configArea, {
|
||||
mode: 'text/yaml',
|
||||
lineNumbers: true,
|
||||
theme: 'dracula',
|
||||
lineWrapping: true,
|
||||
matchBrackets: true
|
||||
});
|
||||
editor.getWrapperElement().style.fontSize = '15px';
|
||||
editor.getWrapperElement().style.lineHeight = '1.5';
|
||||
// Twenty lines at 1.5em, plus CodeMirror's vertical content padding.
|
||||
editor.setSize(null, 'calc(30em + 8px)');
|
||||
editor.refresh();
|
||||
showConfig();
|
||||
}).catch(function() {
|
||||
message.textContent = _('Unable to load editor');
|
||||
});
|
||||
return page;
|
||||
}
|
||||
});
|
||||
@@ -1,204 +0,0 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
var callCoreInfo = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'getCoreInfo',
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: [ 'name', 'verbose' ],
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
var callRedirectStatus = rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'getRedirectStatus',
|
||||
expect: { '': {} },
|
||||
reject: true
|
||||
});
|
||||
|
||||
function modeLabel(mode) {
|
||||
return {
|
||||
none: _('Do not handle DNS'),
|
||||
'dnsmasq-upstream': _('Run as dnsmasq upstream server'),
|
||||
redirect: _('Forward port 53 requests through the firewall'),
|
||||
exchange: _('Use port 53 instead of dnsmasq')
|
||||
}[mode] || _('Status unavailable');
|
||||
}
|
||||
|
||||
function coreIsRunning(services, binpath) {
|
||||
var instances = services.AdGuardHome && services.AdGuardHome.instances;
|
||||
if (!instances)
|
||||
return false;
|
||||
|
||||
return Object.keys(instances).some(function(name) {
|
||||
var instance = instances[name];
|
||||
return instance.running === true && Array.isArray(instance.command) && instance.command[0] === binpath;
|
||||
});
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
render: function() {
|
||||
var badgeStyle = 'display:inline-flex;align-items:center;padding:0.4em 0.8em;' +
|
||||
'border:1px solid;border-radius:999px;font-weight:600;line-height:1.4;';
|
||||
var coreBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
|
||||
var serviceBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
|
||||
var redirectBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
|
||||
var node = E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:0.6em;padding:0.6em;' }, [
|
||||
coreBadge, serviceBadge, redirectBadge
|
||||
])
|
||||
]);
|
||||
|
||||
function setBadge(badge, label, state) {
|
||||
badge.textContent = label;
|
||||
badge.style.backgroundColor = state === 'pending' ? '#fef3c7' : state === true ? '#dcfce7' : state === false ? '#fee2e2' : '#e5e7eb';
|
||||
badge.style.color = state === 'pending' ? '#92400e' : state === true ? '#14532d' : state === false ? '#7f1d1d' : '#374151';
|
||||
badge.style.borderColor = state === 'pending' ? '#fcd34d' : state === true ? '#86efac' : state === false ? '#fca5a5' : '#d1d5db';
|
||||
}
|
||||
|
||||
function refreshCore() {
|
||||
return callCoreInfo().then(function(info) {
|
||||
if (!info.core_exists)
|
||||
setBadge(coreBadge, _('Core') + ': ' + _('no core'), false);
|
||||
else if (!info.version)
|
||||
setBadge(coreBadge, _('Core') + ': ' + _('core error'), false);
|
||||
else if (!info.config_exists)
|
||||
setBadge(coreBadge, _('Core') + ': ' + info.version + ' (' + _('no config') + ')', false);
|
||||
else
|
||||
setBadge(coreBadge, _('Core') + ': ' + info.version, true);
|
||||
}).catch(function() {
|
||||
setBadge(coreBadge, _('Core') + ': ' + _('Status unavailable'), null);
|
||||
});
|
||||
}
|
||||
|
||||
var storageKey = 'adguardhome.pendingSwitch';
|
||||
var pending = null;
|
||||
var generation = 0;
|
||||
var stable = 0;
|
||||
var nextRefresh = 0;
|
||||
var inFlight = null;
|
||||
try {
|
||||
var saved = JSON.parse(sessionStorage.getItem(storageKey));
|
||||
if (saved && typeof saved.mode === 'string' && typeof saved.enabled === 'boolean' &&
|
||||
typeof saved.started === 'number')
|
||||
pending = saved;
|
||||
} catch (e) {}
|
||||
|
||||
function remember() {
|
||||
try {
|
||||
if (pending)
|
||||
sessionStorage.setItem(storageKey, JSON.stringify(pending));
|
||||
else
|
||||
sessionStorage.removeItem(storageKey);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function showPending(running) {
|
||||
var label = Date.now() - pending.started >= 30000
|
||||
? _('Switch not completed; check the log') : _('Switching');
|
||||
if (typeof running === 'boolean')
|
||||
setBadge(serviceBadge, running ? _('RUNNING') : _('NOT RUNNING'), running);
|
||||
else
|
||||
setBadge(serviceBadge, _('Checking service…'), 'pending');
|
||||
setBadge(redirectBadge, label, 'pending');
|
||||
}
|
||||
|
||||
function beginSwitch(target) {
|
||||
generation++;
|
||||
stable = 0;
|
||||
nextRefresh = 0;
|
||||
pending = { mode: target.mode, enabled: target.enabled, started: Date.now() };
|
||||
remember();
|
||||
showPending();
|
||||
}
|
||||
|
||||
function cancelSwitch() {
|
||||
generation++;
|
||||
pending = null;
|
||||
stable = 0;
|
||||
nextRefresh = 0;
|
||||
remember();
|
||||
return refreshStatus();
|
||||
}
|
||||
|
||||
function matches(info, running, target) {
|
||||
return !info.busy && info.mode === target.mode && info.enabled === target.enabled &&
|
||||
info.applied_enabled === target.enabled && running === target.enabled &&
|
||||
(!target.enabled || info.applied_mode === target.mode) &&
|
||||
info.redirected === (target.enabled && target.mode !== 'none');
|
||||
}
|
||||
|
||||
function refreshStatus() {
|
||||
if (inFlight)
|
||||
return inFlight;
|
||||
var currentGeneration = generation;
|
||||
var requestedAt = Date.now();
|
||||
inFlight = Promise.all([callRedirectStatus(), callServiceList('AdGuardHome', true)]).then(function(results) {
|
||||
if (currentGeneration !== generation)
|
||||
return;
|
||||
var info = results[0];
|
||||
var running = coreIsRunning(results[1], info.binpath);
|
||||
// Recover switching feedback after navigation or an apply from another page.
|
||||
if (!pending && (info.busy || info.mode !== info.applied_mode || info.enabled !== info.applied_enabled))
|
||||
beginSwitch(info);
|
||||
if (pending) {
|
||||
stable = matches(info, running, pending) ? stable + 1 : 0;
|
||||
// Two consecutive samples avoid declaring a short-lived process healthy.
|
||||
if (stable < 2) {
|
||||
showPending(running);
|
||||
return;
|
||||
}
|
||||
pending = null;
|
||||
remember();
|
||||
}
|
||||
setBadge(serviceBadge, running ? _('RUNNING') : _('NOT RUNNING'), running);
|
||||
var active = info.redirected && running && info.mode === info.applied_mode;
|
||||
var label = modeLabel(info.mode);
|
||||
if (!info.enabled)
|
||||
label += ' (' + _('Disabled') + ')';
|
||||
else if (info.mode !== 'none' && !active)
|
||||
label += ' (' + _('Not redirect') + ')';
|
||||
setBadge(redirectBadge, label,
|
||||
!info.enabled || info.mode === 'none' ? null : active);
|
||||
}).catch(function() {
|
||||
if (currentGeneration !== generation)
|
||||
return;
|
||||
stable = 0;
|
||||
if (pending) {
|
||||
showPending();
|
||||
setBadge(serviceBadge, _('Status unavailable'), null);
|
||||
setBadge(redirectBadge, _('Unable to read switch status') + ' (' + modeLabel(pending.mode) + ')', null);
|
||||
} else {
|
||||
setBadge(serviceBadge, _('Status unavailable'), null);
|
||||
setBadge(redirectBadge, _('Status unavailable'), null);
|
||||
}
|
||||
}).then(function() {
|
||||
nextRefresh = requestedAt + (pending ? 1000 : 3000);
|
||||
inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
if (pending)
|
||||
showPending();
|
||||
refreshCore();
|
||||
refreshStatus();
|
||||
poll.add(function() {
|
||||
if (Date.now() >= nextRefresh)
|
||||
return refreshStatus();
|
||||
}, 1);
|
||||
|
||||
return { node: node, refresh: function() {
|
||||
return Promise.all([refreshCore(), refreshStatus()]);
|
||||
}, beginSwitch: beginSwitch, cancelSwitch: cancelSwitch };
|
||||
}
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$SDK_DIR"
|
||||
|
||||
mkdir -p package/${PKG_DIR_NAME}
|
||||
rsync -a --delete "$GITHUB_WORKSPACE/" "package/${PKG_DIR_NAME}/" \
|
||||
--exclude '.git*' --exclude '.local' --exclude '_sdk' --exclude 'dist'
|
||||
|
||||
./scripts/feeds update -a
|
||||
./scripts/feeds install luci-base tar
|
||||
|
||||
touch .config
|
||||
sed -i -E '/^(# )?CONFIG_(ALL|ALL_KMODS|ALL_NONSHARED|LUCI_LANG_zh_Hans|PACKAGE_TAR_XZ|PACKAGE_luci-app-adguardhome-tk|PACKAGE_luci-i18n-adguardhome-tk-zh-cn)(=| is not set)/d' .config
|
||||
printf '%s\n' \
|
||||
'CONFIG_ALL=n' \
|
||||
'CONFIG_ALL_KMODS=n' \
|
||||
'CONFIG_ALL_NONSHARED=n' \
|
||||
'CONFIG_LUCI_LANG_zh_Hans=y' \
|
||||
'# CONFIG_PACKAGE_TAR_XZ is not set' \
|
||||
'CONFIG_PACKAGE_luci-app-adguardhome-tk=y' >> .config
|
||||
make defconfig
|
||||
if ! grep -Eq '^CONFIG_PACKAGE_luci-app-adguardhome-tk=(y|m)$' .config; then
|
||||
echo '::error::luci-app-adguardhome-tk was not enabled after make defconfig'
|
||||
grep -n 'luci-app-adguardhome-tk' .config || true
|
||||
sed -n '/^[[:space:]]*config PACKAGE_luci-app-adguardhome-tk$/,/^[[:space:]]*help/p' tmp/.config-package.in
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -Eq '^CONFIG_PACKAGE_luci-i18n-adguardhome-tk-zh-cn=(y|m)$' .config || {
|
||||
echo '::error::Simplified Chinese translation package was not enabled'
|
||||
exit 1
|
||||
}
|
||||
|
||||
make package/compile -j"$(nproc)" V=sc
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
mkdir -p "$GITHUB_WORKSPACE/dist"
|
||||
for package in "$PKG_DIR_NAME" luci-i18n-adguardhome-tk-zh-cn; do
|
||||
mapfile -t packages < <(find "$SDK_DIR/bin/packages" -type f -name "${package}-*.apk")
|
||||
if (( ${#packages[@]} == 0 )); then
|
||||
echo "::error::Missing APK: ${package}"
|
||||
exit 1
|
||||
fi
|
||||
cp "${packages[@]}" "$GITHUB_WORKSPACE/dist/"
|
||||
done
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
REPO="w9315273/luci-app-adguardhome"
|
||||
LANGUAGE=${1:-en}
|
||||
case "$LANGUAGE" in
|
||||
en|zh-cn) ;;
|
||||
*) echo "Usage: sh install.sh [en|zh-cn]"; exit 1 ;;
|
||||
esac
|
||||
[ "$#" -le 1 ] || { echo "Usage: sh install.sh [en|zh-cn]"; exit 1; }
|
||||
|
||||
echo "Getting latest version..."
|
||||
RELEASE=$(uclient-fetch -qO- "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null)
|
||||
TAG=$(printf '%s\n' "$RELEASE" | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
|
||||
|
||||
[ -n "$TAG" ] || { echo "ERROR: Failed to get latest version"; exit 1; }
|
||||
|
||||
case "$TAG" in
|
||||
*-*) ;;
|
||||
*) echo "ERROR: Unexpected tag format: $TAG"; exit 1 ;;
|
||||
esac
|
||||
|
||||
VERSION=${TAG#v}
|
||||
VERSION=${VERSION%-*}-r${VERSION##*-}
|
||||
|
||||
PACKAGE=luci-app-adguardhome
|
||||
I18N_PACKAGE=luci-i18n-adguardhome-zh-cn
|
||||
if printf '%s\n' "$RELEASE" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"luci-app-adguardhome-tk-[^"]*\.apk"'; then
|
||||
PACKAGE=luci-app-adguardhome-tk
|
||||
I18N_PACKAGE=luci-i18n-adguardhome-tk-zh-cn
|
||||
fi
|
||||
URL="https://github.com/${REPO}/releases/download/${TAG}/${PACKAGE}-${VERSION}.apk"
|
||||
|
||||
echo "Version: $VERSION"
|
||||
echo "Downloading..."
|
||||
work=$(mktemp -d /tmp/agh-install.XXXXXX)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
uclient-fetch -qO "$work/main.apk" "$URL"
|
||||
set -- "$work/main.apk"
|
||||
|
||||
I18N="${I18N_PACKAGE}-${VERSION}.apk"
|
||||
has_i18n=0
|
||||
if [ "$LANGUAGE" = zh-cn ] && printf '%s\n' "$RELEASE" | grep -Eq "\"name\"[[:space:]]*:[[:space:]]*\"${I18N_PACKAGE}-[^\"]*\\.apk\""; then
|
||||
uclient-fetch -qO "$work/zh-cn.apk" "https://github.com/${REPO}/releases/download/${TAG}/${I18N}"
|
||||
set -- "$@" "$work/zh-cn.apk"
|
||||
has_i18n=1
|
||||
fi
|
||||
|
||||
if [ "$LANGUAGE" = zh-cn ] && [ "$PACKAGE" = luci-app-adguardhome-tk ] && [ "$has_i18n" != 1 ]; then
|
||||
echo "ERROR: The release is missing the Chinese package"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
legacy_packages=
|
||||
if [ "$PACKAGE" = luci-app-adguardhome-tk ]; then
|
||||
installed=$(apk info)
|
||||
for old in luci-app-adguardhome luci-i18n-adguardhome-zh-cn; do
|
||||
if printf '%s\n' "$installed" | grep -qx "$old"; then
|
||||
if [ "$old" != luci-i18n-adguardhome-zh-cn ] || [ "$LANGUAGE" = zh-cn ]; then
|
||||
set -- "$@" "$old"
|
||||
fi
|
||||
legacy_packages="$legacy_packages $old"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Installing..."
|
||||
apk add --allow-untrusted "$@"
|
||||
if [ -n "$legacy_packages" ]; then
|
||||
apk del $legacy_packages
|
||||
fi
|
||||
|
||||
if [ "$LANGUAGE" = en ]; then
|
||||
installed=$(apk info)
|
||||
for translation in luci-i18n-adguardhome-zh-cn luci-i18n-adguardhome-tk-zh-cn; do
|
||||
if printf '%s\n' "$installed" | grep -qx "$translation"; then
|
||||
apk del "$translation"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Refreshing LuCI..."
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null
|
||||
/etc/init.d/rpcd reload >/dev/null 2>&1
|
||||
|
||||
echo "Done."
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$SDK_DIR"
|
||||
|
||||
if [[ ! -x scripts/feeds ]]; then
|
||||
runuser -u buildbot -- bash ./setup.sh
|
||||
fi
|
||||
|
||||
mkdir -p dl
|
||||
chown -R buildbot:buildbot dl
|
||||
Reference in new issue
Block a user