luci-app-adguardhome: add new app
authorGeorge Sapkin <redacted>
Thu, 12 Mar 2026 00:28:54 +0000 (02:28 +0200)
committerPaul Donald <redacted>
Mon, 16 Mar 2026 20:00:18 +0000 (21:00 +0100)
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 <redacted>
applications/luci-app-adguardhome/Makefile [new file with mode: 0644]
applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js [new file with mode: 0644]
applications/luci-app-adguardhome/po/templates/adguardhome.pot [new file with mode: 0644]
applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json [new file with mode: 0644]
applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json [new file with mode: 0644]
applications/luci-app-adguardhome/root/usr/share/ucitrack/luci-app-adguardhome.json [new file with mode: 0644]

diff --git a/applications/luci-app-adguardhome/Makefile b/applications/luci-app-adguardhome/Makefile
new file mode 100644 (file)
index 0000000..a48c04d
--- /dev/null
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+include $(TOPDIR)/rules.mk
+
+LUCI_NAME:=luci-app-adguardhome
+LUCI_MAINTAINER:=George Sapkin <george@sapk.in>
+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 (file)
index 0000000..6dc2d45
--- /dev/null
@@ -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 = `<span style="color: var(--success-color-high); font-weight: bold">${_('Running')}</span>`;
+const NOT_RUNNING_SPAN = `<span style="color: var(--error-color-high); font-weight: bold">${_('Not running')}</span>`;
+
+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\'.') +
+                               '<br />' + _('Parent directory will be owned by the service user.') +
+                               '<br />' + _('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.') +
+                               '<br />' + _('Will be owned by the service user.') +
+                               '<br />' + _('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.') + '<br />' +
+                               _('If empty, defaults to') + ' ' + _('unset and 100') + '.',
+                               '<a href="https://go.dev/doc/gc-guide#GOGC" target="_blank">https://go.dev/doc/gc-guide#GOGC</a>'
+               );
+               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.') + '<br />' +
+                               _('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.') +
+                               '<br />' +
+                               _('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 (file)
index 0000000..2120d26
--- /dev/null
@@ -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 (file)
index 0000000..f4dddb5
--- /dev/null
@@ -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 (file)
index 0000000..748778a
--- /dev/null
@@ -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 (file)
index 0000000..003c088
--- /dev/null
@@ -0,0 +1,4 @@
+{
+       "config": "adguardhome",
+       "init": "adguardhome"
+}
git clone https://git.99rst.org/PROJECT