From: George Sapkin Date: Thu, 12 Mar 2026 00:28:54 +0000 (+0200) Subject: luci-app-adguardhome: add new app X-Git-Url: http://git.99rst.org/?a=commitdiff_plain;h=c923488ddaafb74adbe0cc0dbe0c53d5826e1d30;p=openwrt-luci.git luci-app-adguardhome: add new app Add LuCI UI for AdGuard Home configuration. If AdGuard Home service is running, restart it automatically when configuration is applied. Signed-off-by: George Sapkin --- diff --git a/applications/luci-app-adguardhome/Makefile b/applications/luci-app-adguardhome/Makefile new file mode 100644 index 0000000000..a48c04d37c --- /dev/null +++ b/applications/luci-app-adguardhome/Makefile @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only + +include $(TOPDIR)/rules.mk + +LUCI_NAME:=luci-app-adguardhome +LUCI_MAINTAINER:=George Sapkin +PKG_LICENSE:=GPL-2.0-only + +LUCI_TITLE:=LuCI support for AdGuard Home +LUCI_DEPENDS:=+adguardhome +luci-base +LUCI_EXTRA_DEPENDS:=adguardhome (>=0.107.73-r3) +LUCI_PKGARCH:=all + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js new file mode 100644 index 0000000000..6dc2d45056 --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js @@ -0,0 +1,261 @@ +'use strict'; + +'require dom'; +'require form'; +'require fs'; +'require poll'; +'require rpc'; +'require view'; + +const DEFAULT_CONFIG_FILE = '/etc/adguardhome/adguardhome.yaml'; +const DEFAULT_WORK_DIR = '/var/lib/adguardhome'; +const DEFAULT_USER = 'adguardhome'; +const DEFAULT_GROUP = DEFAULT_USER; + +const DEFAULT_GOGC = '0'; +const DEFAULT_GOMAXPROCS = '0'; +const DEFAULT_GOMEMLIMIT = '0'; + +const PATH_REGEX = new RegExp('^/etc(/[^/]+)?/?$'); + +const POLL_INTERVAL = 5; + +const RUNNING_SPAN = `${_('Running')}`; +const NOT_RUNNING_SPAN = `${_('Not running')}`; + +const STORAGE_KEY = 'luci-app-adguardhome'; + +function getServiceInfo(name) { + const fn = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { [name]: { instances: { [name]: {} }}}, + }); + return () => fn(name); +} + +const getAGHServiceInfo = getServiceInfo('adguardhome'); + +async function getStatus() { + try { + const res = await getAGHServiceInfo(); + const isRunning = res?.instances?.adguardhome?.running; + return isRunning ?? false; + } catch (e) { + console.error(e); + return false; + } +} + +function getStatusValue(isRunning) { + return isRunning ? RUNNING_SPAN : NOT_RUNNING_SPAN; +} + +async function getVersion() { + try { + const res = await fs.exec('/usr/bin/AdGuardHome', ['--version']); + const version = res.stdout + ? (res.stdout.match(/version\s+(.*)/) || [null, res.stdout.trim()])[1] + : ''; + return version; + } catch (e) { + console.error(e); + return 'unknown version'; + } +} + +function updateStatus(node) { + const output = node?.querySelector('output'); + return output + ? async () => { + const isRunning = await getStatus(); + dom.content(output, getStatusValue(isRunning)); + } + : () => {}; +} + +function validateConfigFile(_unused, value) { + if (value == null || value === '') { + return true; + } + if (!value.startsWith('/')) { + return _('Path must be absolute.'); + } + if (value.endsWith('/')) { + return _('Path must not end with a slash.'); + } + if (PATH_REGEX.test(value)) { + return _('Configuration file must be stored in its own directory, and not in \'/etc\'.'); + } + return true; +} + +function validateWorkDir(_unused, value) { + if (value == null || value === '') { + return true; + } + if (!value.startsWith('/')) { + return _('Path must be absolute.'); + } + return true; +} + +return view.extend({ + load() { + return Promise.all([ + getStatus(), + getVersion(), + ]); + }, + + async render([isRunning, version]) { + const map = new form.Map('adguardhome', _('AdGuard Home')); + + const statusSect = map.section(form.TypedSection, 'status'); + statusSect.anonymous = true; + statusSect.cfgsections = () => ['status_section']; + + const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version')); + versionOpt.cfgvalue = () => version; + + const statusOpt = statusSect.option(form.DummyValue, '_status', _('Service Status')); + statusOpt.rawhtml = true; + statusOpt.cfgvalue = () => getStatusValue(isRunning); + + const mainSect = map.section(form.TypedSection, 'adguardhome'); + mainSect.anonymous = true; + + mainSect.tab('general', _('General Settings')); + mainSect.tab( + 'jail', + _('File System Access'), + _('Files and directories that AdGuard Home should have read-only or read-write access to.'), + ); + mainSect.tab( + 'advanced', + _('Advanced Settings'), + _('Go environment variables that tune garbage collector and memory management.') + + ' ' + _('Modify at your own risk.'), + ); + + const configFileOpt = mainSect.taboption( + 'general', + form.Value, + 'config_file', + _('Configuration file'), + _('Configuration file must be stored in its own directory, and not in \'/etc\'.') + + '
' + _('Parent directory will be owned by the service user.') + + '
' + _('If empty, defaults to') + ` '${DEFAULT_CONFIG_FILE}'.`, + ); + configFileOpt.placeholder = DEFAULT_CONFIG_FILE; + configFileOpt.validate = validateConfigFile; + + const workDirOpt = mainSect.taboption( + 'general', + form.Value, + 'work_dir', + _('Working directory'), + _('Directory where filters, logs, and statistics are stored.') + + '
' + _('Will be owned by the service user.') + + '
' + _('If empty, defaults to') + ` '${DEFAULT_WORK_DIR}'.`, + ); + workDirOpt.placeholder = DEFAULT_WORK_DIR; + workDirOpt.validate = validateWorkDir; + + const userOpt = mainSect.taboption( + 'general', + form.Value, + 'user', + _('Service user'), + _('User the service runs under.') + ' ' + _('If empty, defaults to') + + ` '${DEFAULT_USER}'.`, + ); + userOpt.placeholder = DEFAULT_USER; + + const groupOpt = mainSect.taboption( + 'general', + form.Value, + 'group', + _('Service group'), + _('Group the service runs under.') + ' ' + _('If empty, defaults to') + + ` '${DEFAULT_GROUP}'.`, + + ); + groupOpt.placeholder = DEFAULT_GROUP; + + const verboseOpt = mainSect.taboption( + 'general', + form.Flag, + 'verbose', + _('Verbose logging'), + ); + verboseOpt.default = '0'; + + const advSettingsOpt = mainSect.taboption( + 'general', + form.Flag, + 'advanced_settings', + _('Advanced Settings'), + ); + advSettingsOpt.default = '0'; + advSettingsOpt.rmempty = false; + advSettingsOpt.load = () => sessionStorage.getItem(STORAGE_KEY) || '0'; + advSettingsOpt.remove = () => {}; + advSettingsOpt.write = (_, value) => sessionStorage.setItem(STORAGE_KEY, value); + + mainSect.taboption('jail', form.DynamicList, 'jail_mount', _('Read-only access')); + mainSect.taboption('jail', form.DynamicList, 'jail_mount_rw', _('Read-write access')); + + const gcOpt = mainSect.taboption( + 'advanced', + form.Value, + 'gc', + 'GOGC', + _('Tunes the garbage collector\'s aggressiveness by setting the percentage of heap ' + + 'growth allowed before the next collection cycle triggers.') + '
' + + _('If empty, defaults to') + ' ' + _('unset and 100') + '.', + 'https://go.dev/doc/gc-guide#GOGC' + ); + gcOpt.datatype = 'uinteger'; + gcOpt.depends('advanced_settings', '1'); + gcOpt.placeholder = DEFAULT_GOGC; + gcOpt.retain = true; + + const maxProcsOpt = mainSect.taboption( + 'advanced', + form.Value, + 'maxprocs', + 'GOMAXPROCS', + _('The maximum number of operating system threads that can execute user-level Go code' + + ' simultaneously.') + '
' + + _('If empty, defaults to') + ' ' + _('unset and matching the number of CPUs') + '.', + ); + maxProcsOpt.datatype = 'uinteger'; + maxProcsOpt.depends('advanced_settings', '1'); + maxProcsOpt.placeholder = DEFAULT_GOMAXPROCS; + maxProcsOpt.retain = true; + + const memLimitOpt = mainSect.taboption( + 'advanced', + form.Value, + 'memlimit', + 'GOMEMLIMIT', + _('A soft memory cap for the Go runtime, allowing the garbage collector to run more ' + + 'frequently as usage approaches the limit to prevent Out-of-Memory (OOM) kills.') + + '
' + + _('If empty, defaults to') + ' ' + _('unset') + '.', + ); + memLimitOpt.datatype = 'uinteger'; + memLimitOpt.depends('advanced_settings', '1'); + memLimitOpt.placeholder = DEFAULT_GOMEMLIMIT; + memLimitOpt.retain = true; + + const rendered = await map.render(); + + const statusNode = map.findElement('data-field', statusOpt.cbid('status_section')); + poll.add(updateStatus(statusNode), POLL_INTERVAL); + + return rendered; + }, +}); diff --git a/applications/luci-app-adguardhome/po/templates/adguardhome.pot b/applications/luci-app-adguardhome/po/templates/adguardhome.pot new file mode 100644 index 0000000000..2120d2646e --- /dev/null +++ b/applications/luci-app-adguardhome/po/templates/adguardhome.pot @@ -0,0 +1,159 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244 +msgid "" +"A soft memory cap for the Go runtime, allowing the garbage collector to run " +"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) " +"kills." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113 +#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3 +msgid "AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146 +msgid "Configuration file" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147 +msgid "" +"Configuration file must be stored in its own directory, and not in '/etc'." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159 +msgid "Directory where filters, logs, and statistics are stored." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132 +msgid "File System Access" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133 +msgid "" +"Files and directories that AdGuard Home should have read-only or read-write " +"access to." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138 +msgid "" +"Go environment variables that tune garbage collector and memory management." +msgstr "" + +#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3 +msgid "Grant permissions for the AdGuard Home LuCI app" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181 +msgid "Group the service runs under." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247 +msgid "If empty, defaults to" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139 +msgid "Modify at your own risk." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24 +msgid "Not running" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148 +msgid "Parent directory will be owned by the service user." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99 +msgid "Path must be absolute." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86 +msgid "Path must not end with a slash." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207 +msgid "Read-only access" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208 +msgid "Read-write access" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23 +msgid "Running" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122 +msgid "Service Status" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180 +msgid "Service group" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170 +msgid "Service user" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230 +msgid "" +"The maximum number of operating system threads that can execute user-level " +"Go code simultaneously." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215 +msgid "" +"Tunes the garbage collector's aggressiveness by setting the percentage of " +"heap growth allowed before the next collection cycle triggers." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171 +msgid "User the service runs under." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191 +msgid "Verbose logging" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119 +msgid "Version" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160 +msgid "Will be owned by the service user." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158 +msgid "Working directory" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247 +msgid "unset" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217 +msgid "unset and 100" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232 +msgid "unset and matching the number of CPUs" +msgstr "" diff --git a/applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json b/applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json new file mode 100644 index 0000000000..f4dddb53a1 --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json @@ -0,0 +1,15 @@ +{ + "admin/services/adguardhome": { + "title": "AdGuard Home", + "action": { + "type": "view", + "path": "adguardhome/config" + }, + "depends": { + "acl": [ "luci-app-adguardhome" ], + "uci": { + "adguardhome": true + } + } + } +} diff --git a/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json b/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json new file mode 100644 index 0000000000..748778a5d1 --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json @@ -0,0 +1,17 @@ +{ + "luci-app-adguardhome": { + "description": "Grant permissions for the AdGuard Home LuCI app", + "read": { + "file": { + "/usr/bin/AdGuardHome --version": [ "exec" ] + }, + "ubus": { + "service": [ "list" ] + }, + "uci": [ "adguardhome" ] + }, + "write": { + "uci": [ "adguardhome" ] + } + } +} diff --git a/applications/luci-app-adguardhome/root/usr/share/ucitrack/luci-app-adguardhome.json b/applications/luci-app-adguardhome/root/usr/share/ucitrack/luci-app-adguardhome.json new file mode 100644 index 0000000000..003c0887f2 --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/share/ucitrack/luci-app-adguardhome.json @@ -0,0 +1,4 @@ +{ + "config": "adguardhome", + "init": "adguardhome" +}