From 2d28b4b752f0042143b5352c37e88b07ef8f7d34 Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sat, 23 May 2026 02:01:37 -0400 Subject: [PATCH] Development --- ...n_dnsserver.py => action_dnsblocklists.py} | 22 +- docker/routlin-dash/app/action_general.py | 109 ---- ..._config.py => action_networkinterfaces.py} | 60 ++- docker/routlin-dash/app/action_upstreamdns.py | 61 +++ docker/routlin-dash/app/main.py | 10 +- docker/routlin-dash/app/view_page.py | 23 +- docker/routlin-dash/data/navbar_content.json | 6 +- docker/routlin-dash/data/page_content.json | 474 ++++++++++-------- 8 files changed, 414 insertions(+), 351 deletions(-) rename docker/routlin-dash/app/{action_dnsserver.py => action_dnsblocklists.py} (87%) rename docker/routlin-dash/app/{action_apply_iface_config.py => action_networkinterfaces.py} (55%) create mode 100644 docker/routlin-dash/app/action_upstreamdns.py diff --git a/docker/routlin-dash/app/action_dnsserver.py b/docker/routlin-dash/app/action_dnsblocklists.py similarity index 87% rename from docker/routlin-dash/app/action_dnsserver.py rename to docker/routlin-dash/app/action_dnsblocklists.py index fef8fd1..792ddda 100644 --- a/docker/routlin-dash/app/action_dnsserver.py +++ b/docker/routlin-dash/app/action_dnsblocklists.py @@ -5,7 +5,7 @@ import re import sanitize import validation as validate -bp = Blueprint('action_dnsserver', __name__) +bp = Blueprint('action_dnsblocklists', __name__) VIEW = '/view/view_dns_server' @@ -51,9 +51,9 @@ def _parse_fields(): return {'name': name, 'description': description, 'format': fmt, 'url': url}, None -@bp.route('/action/dnsserver_tableblocklists_rowdelete', methods=['POST']) +@bp.route('/action/dnsblocklists_tableblocklists_rowdelete', methods=['POST']) @require_level('administrator') -def dnsserver_tableblocklists_rowdelete(): +def dnsblocklists_tableblocklists_rowdelete(): idx = _row_index() if idx is None: flash('Invalid request.', 'error') @@ -80,9 +80,9 @@ def dnsserver_tableblocklists_rowdelete(): return redirect(VIEW) -@bp.route('/action/dnsserver_tableblocklist_rowedit', methods=['POST']) +@bp.route('/action/dnsblocklists_tableblocklist_rowedit', methods=['POST']) @require_level('administrator') -def dnsserver_tableblocklist_rowedit(): +def dnsblocklists_tableblocklist_rowedit(): idx = _row_index() if idx is None: flash('Invalid request.', 'error') @@ -118,9 +118,9 @@ def dnsserver_tableblocklist_rowedit(): return redirect(VIEW) -@bp.route('/action/dnsserver_cardaddblocklist_add', methods=['POST']) +@bp.route('/action/dnsblocklists_cardaddblocklist_add', methods=['POST']) @require_level('administrator') -def dnsserver_cardaddblocklist_add(): +def dnsblocklists_cardaddblocklist_add(): fields, err = _parse_fields() if err: return redirect(VIEW) @@ -153,9 +153,9 @@ def dnsserver_cardaddblocklist_add(): return redirect(VIEW) -@bp.route('/action/dnsserver_cardblocklistrefresh_save', methods=['POST']) +@bp.route('/action/dnsblocklists_cardblocklistrefresh_save', methods=['POST']) @require_level('administrator') -def dnsserver_cardblocklistrefresh_save(): +def dnsblocklists_cardblocklistrefresh_save(): daily_execute_time = sanitize.time_24h(request.form.get('daily_execute_time_24hr_local', '')) if not verify_core_hash(request.form.get('config_hash', '')): @@ -170,8 +170,8 @@ def dnsserver_cardblocklistrefresh_save(): return redirect(VIEW) -@bp.route('/action/dnsserver_cardblocklistrefresh_refresh', methods=['POST']) +@bp.route('/action/dnsblocklists_cardblocklistrefresh_refresh', methods=['POST']) @require_level('administrator') -def dnsserver_cardblocklistrefresh_refresh(): +def dnsblocklists_cardblocklistrefresh_refresh(): flash(queued_msg('core update-blocklists'), 'success') return redirect(VIEW) diff --git a/docker/routlin-dash/app/action_general.py b/docker/routlin-dash/app/action_general.py index 4bc7a4c..60941ad 100644 --- a/docker/routlin-dash/app/action_general.py +++ b/docker/routlin-dash/app/action_general.py @@ -1,123 +1,14 @@ -import os - from flask import Blueprint, request, redirect, flash from auth import require_level from config_utils import (load_core, save_core, verify_core_hash, queued_msg, flush_pending_to_queue, get_dashboard_pending, _is_locked, _format_timing, _seconds_until_next_run) -import sanitize import validation as validate bp = Blueprint('action_general', __name__) _VIEW = '/view/view_general' -_EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth', - 'tun', 'tap', 'ppp', 'virbr', - 'podman', 'vnet', 'macvtap', 'fc-') - - -def _get_system_interfaces(): - try: - return { - n for n in os.listdir('/sys/class/net') - if not n.startswith(_EXCLUDE_PREFIXES) - and os.path.exists(f'/sys/class/net/{n}/device') - } - except Exception: - return set() - - -@bp.route('/action/general_cardnetworkinterface_save', methods=['POST']) -@require_level('administrator') -def general_cardnetworkinterface_save(): - wan = sanitize.interface_name(request.form.get('wan_interface', '')) - lan = sanitize.interface_name(request.form.get('lan_interface', '')) - - if not wan or not lan: - flash('Both WAN and LAN interfaces are required.', 'error') - return redirect(_VIEW) - - if wan == lan: - flash('WAN and LAN interfaces must be different.', 'error') - return redirect(_VIEW) - - if not verify_core_hash(request.form.get('config_hash', '')): - flash('Configuration was modified by another session. Please refresh and try again.', 'error') - return redirect(_VIEW) - - available = _get_system_interfaces() - for iface in (wan, lan): - if available and iface not in available: - flash(f"Interface '{iface}' does not exist on this system.", 'error') - return redirect(_VIEW) - - core = load_core() - gen = core.setdefault('general', {}) - gen['wan_interface'] = wan - gen['lan_interface'] = lan - errors = validate.validate_config(core) - if errors: - for msg in errors: - flash(msg, 'error') - return redirect(_VIEW) - save_core(core) - - flash(queued_msg('core apply'), 'success') - return redirect(_VIEW) - - -@bp.route('/action/general_cardupstreamdns_save', methods=['POST']) -@require_level('administrator') -def general_cardupstreamdns_save(): - strict_order = 'strict_order' in request.form - cache_size_raw = request.form.get('cache_size', '').strip() - submitted = request.form.getlist('upstream_servers') - - for s in submitted: - if not s.strip(): - flash('Remove blank server entries before saving.', 'error') - return redirect(_VIEW) - - upstream_servers = [] - for s in submitted: - clean = sanitize.ip(s.strip()) - if not clean: - flash(f"'{s.strip()}' is not a valid IP address.", 'error') - return redirect(_VIEW) - upstream_servers.append(clean) - - cache_size = validate.int_range(cache_size_raw, 0, None) - if cache_size is None: - flash('Cache Size must be a non-negative integer.', 'error') - return redirect(_VIEW) - - if not verify_core_hash(request.form.get('config_hash', '')): - flash('Configuration was modified by another session. Please refresh and try again.', 'error') - return redirect(_VIEW) - - core = load_core() - current = core.get('upstream_dns', {}) - if (strict_order == bool(current.get('strict_order', False)) and - cache_size == int(current.get('cache_size', 0)) and - upstream_servers == current.get('upstream_servers', [])): - flash('No changes detected.', 'info') - return redirect(_VIEW) - - core.setdefault('upstream_dns', {}).update({ - 'strict_order': strict_order, - 'cache_size': cache_size, - 'upstream_servers': upstream_servers, - }) - errors = validate.validate_config(core) - if errors: - for msg in errors: - flash(msg, 'error') - return redirect(_VIEW) - save_core(core) - flash(queued_msg('core apply'), 'success') - return redirect(_VIEW) - @bp.route('/action/general_cardlogging_save', methods=['POST']) @require_level('administrator') diff --git a/docker/routlin-dash/app/action_apply_iface_config.py b/docker/routlin-dash/app/action_networkinterfaces.py similarity index 55% rename from docker/routlin-dash/app/action_apply_iface_config.py rename to docker/routlin-dash/app/action_networkinterfaces.py index 51f5a47..c82e237 100644 --- a/docker/routlin-dash/app/action_apply_iface_config.py +++ b/docker/routlin-dash/app/action_networkinterfaces.py @@ -2,32 +2,76 @@ import os from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import verify_core_hash, queued_msg, queue_command +from config_utils import load_core, save_core, verify_core_hash, queued_msg, queue_command import sanitize import validation as validate -bp = Blueprint('action_apply_iface_config', __name__) +bp = Blueprint('action_networkinterfaces', __name__) -_VIEW = '/view/view_general' +_VIEW = '/view/view_network_interfaces' _EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth', 'tun', 'tap', 'ppp', 'virbr', 'podman', 'vnet', 'macvtap', 'fc-') -def _valid_interface(name): + +def _get_system_interfaces(): try: - return name in { + return { n for n in os.listdir('/sys/class/net') if not n.startswith(_EXCLUDE_PREFIXES) and os.path.exists(f'/sys/class/net/{n}/device') } except Exception: - return False + return set() -@bp.route('/action/apply_iface_config', methods=['POST']) +def _valid_interface(name): + return name in _get_system_interfaces() + + +@bp.route('/action/networkinterfaces_cardnetworkinterface_save', methods=['POST']) @require_level('administrator') -def apply_iface_config(): +def networkinterfaces_cardnetworkinterface_save(): + wan = sanitize.interface_name(request.form.get('wan_interface', '')) + lan = sanitize.interface_name(request.form.get('lan_interface', '')) + + if not wan or not lan: + flash('Both WAN and LAN interfaces are required.', 'error') + return redirect(_VIEW) + + if wan == lan: + flash('WAN and LAN interfaces must be different.', 'error') + return redirect(_VIEW) + + if not verify_core_hash(request.form.get('config_hash', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(_VIEW) + + available = _get_system_interfaces() + for iface in (wan, lan): + if available and iface not in available: + flash(f"Interface '{iface}' does not exist on this system.", 'error') + return redirect(_VIEW) + + core = load_core() + gen = core.setdefault('general', {}) + gen['wan_interface'] = wan + gen['lan_interface'] = lan + errors = validate.validate_config(core) + if errors: + for msg in errors: + flash(msg, 'error') + return redirect(_VIEW) + save_core(core) + + flash(queued_msg('core apply'), 'success') + return redirect(_VIEW) + + +@bp.route('/action/networkinterfaces_cardinterfaceconfiguration_apply', methods=['POST']) +@require_level('administrator') +def networkinterfaces_cardinterfaceconfiguration_apply(): if not verify_core_hash(request.form.get('config_hash', '')): flash('Configuration was modified by another session. Please refresh and try again.', 'error') return redirect(_VIEW) diff --git a/docker/routlin-dash/app/action_upstreamdns.py b/docker/routlin-dash/app/action_upstreamdns.py new file mode 100644 index 0000000..c687e12 --- /dev/null +++ b/docker/routlin-dash/app/action_upstreamdns.py @@ -0,0 +1,61 @@ +from flask import Blueprint, request, redirect, flash +from auth import require_level +from config_utils import load_core, save_core, verify_core_hash, queued_msg +import sanitize +import validation as validate + +bp = Blueprint('action_upstreamdns', __name__) + +_VIEW = '/view/view_upstream_dns' + + +@bp.route('/action/upstreamdns_cardupstreamdns_save', methods=['POST']) +@require_level('administrator') +def upstreamdns_cardupstreamdns_save(): + strict_order = 'strict_order' in request.form + cache_size_raw = request.form.get('cache_size', '').strip() + submitted = request.form.getlist('upstream_servers') + + for s in submitted: + if not s.strip(): + flash('Remove blank server entries before saving.', 'error') + return redirect(_VIEW) + + upstream_servers = [] + for s in submitted: + clean = sanitize.ip(s.strip()) + if not clean: + flash(f"'{s.strip()}' is not a valid IP address.", 'error') + return redirect(_VIEW) + upstream_servers.append(clean) + + cache_size = validate.int_range(cache_size_raw, 0, None) + if cache_size is None: + flash('Cache Size must be a non-negative integer.', 'error') + return redirect(_VIEW) + + if not verify_core_hash(request.form.get('config_hash', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(_VIEW) + + core = load_core() + current = core.get('upstream_dns', {}) + if (strict_order == bool(current.get('strict_order', False)) and + cache_size == int(current.get('cache_size', 0)) and + upstream_servers == current.get('upstream_servers', [])): + flash('No changes detected.', 'info') + return redirect(_VIEW) + + core.setdefault('upstream_dns', {}).update({ + 'strict_order': strict_order, + 'cache_size': cache_size, + 'upstream_servers': upstream_servers, + }) + errors = validate.validate_config(core) + if errors: + for msg in errors: + flash(msg, 'error') + return redirect(_VIEW) + save_core(core) + flash(queued_msg('core apply'), 'success') + return redirect(_VIEW) diff --git a/docker/routlin-dash/app/main.py b/docker/routlin-dash/app/main.py index 6236bb9..364a081 100644 --- a/docker/routlin-dash/app/main.py +++ b/docker/routlin-dash/app/main.py @@ -2,11 +2,13 @@ import os, json, sys from flask import Flask from view_page import bp as view_page_bp from action_general import bp as action_general_bp +from action_networkinterfaces import bp as action_networkinterfaces_bp +from action_upstreamdns import bp as action_upstreamdns_bp from action_apply_mdns import bp as action_apply_mdns_bp from action_apply_vpn import bp as action_apply_vpn_bp from action_apply_banned_ips import bp as action_apply_banned_ips_bp from action_apply_host_overrides import bp as action_apply_host_overrides_bp -from action_dnsserver import bp as action_dnsserver_bp +from action_dnsblocklists import bp as action_dnsblocklists_bp from action_apply_vlans import bp as action_apply_vlans_bp from action_apply_inter_vlan import bp as action_apply_inter_vlan_bp from action_apply_port_forwarding import bp as action_apply_port_forwarding_bp @@ -21,18 +23,19 @@ from action_save_preferences import bp as action_save_preferences_bp from action_change_password import bp as action_change_password_bp from action_clear_ddns_log import bp as action_clear_ddns_log_bp from action_apply_ddns_providers import bp as action_apply_ddns_providers_bp -from action_apply_iface_config import bp as action_apply_iface_config_bp from api_apply_status import bp as api_apply_status_bp app = Flask(__name__) app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24)) app.register_blueprint(view_page_bp) app.register_blueprint(action_general_bp) +app.register_blueprint(action_networkinterfaces_bp) +app.register_blueprint(action_upstreamdns_bp) app.register_blueprint(action_apply_mdns_bp) app.register_blueprint(action_apply_vpn_bp) app.register_blueprint(action_apply_banned_ips_bp) app.register_blueprint(action_apply_host_overrides_bp) -app.register_blueprint(action_dnsserver_bp) +app.register_blueprint(action_dnsblocklists_bp) app.register_blueprint(action_apply_vlans_bp) app.register_blueprint(action_apply_inter_vlan_bp) app.register_blueprint(action_apply_port_forwarding_bp) @@ -47,7 +50,6 @@ app.register_blueprint(action_save_preferences_bp) app.register_blueprint(action_change_password_bp) app.register_blueprint(action_clear_ddns_log_bp) app.register_blueprint(action_apply_ddns_providers_bp) -app.register_blueprint(action_apply_iface_config_bp) app.register_blueprint(api_apply_status_bp) def _seed_initial_account(): diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 957f567..e206712 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -207,6 +207,26 @@ def _fmt_timestamp(ts): except Exception: return '-' +def _relative_time(ts): + try: + diff = int(datetime.now(tz=timezone.utc).timestamp()) - int(ts) + if diff < 60: + n = max(0, diff) + return f'{n} second{"s" if n != 1 else ""} ago' + m = diff // 60 + if m < 60: + return f'{m} minute{"s" if m != 1 else ""} ago' + h = m // 60 + if h < 24: + return f'{h} hour{"s" if h != 1 else ""} ago' + d = h // 24 + if d < 365: + return f'{d} day{"s" if d != 1 else ""} ago' + y = d // 365 + return f'{y} year{"s" if y != 1 else ""} ago' + except Exception: + return '' + def _live_vpn_sessions(): rows = [] out = _run('wg show all dump 2>/dev/null') @@ -408,8 +428,9 @@ def _blocklist_stats_html(core): try: with open(bl_path) as f: entries = sum(1 for _ in f) + mtime = int(os.path.getmtime(bl_path)) size_str = _fmt_bytes(os.path.getsize(bl_path)) - last_refreshed = _fmt_timestamp(int(os.path.getmtime(bl_path))) + last_refreshed = f'{_fmt_timestamp(mtime)} ({_relative_time(mtime)})' except Exception: entries, size_str, last_refreshed = '-', '-', 'Never' rows += (f'' diff --git a/docker/routlin-dash/data/navbar_content.json b/docker/routlin-dash/data/navbar_content.json index 0eb2b0a..3f17e3a 100644 --- a/docker/routlin-dash/data/navbar_content.json +++ b/docker/routlin-dash/data/navbar_content.json @@ -11,8 +11,10 @@ "label": "%MENU_LABEL%", "client_requirement": "client_is_viewer+", "items": [ - { "type": "nav_item", "label": "General", "map_to": "view_general", "client_requirement": "client_is_administrator+" }, - { "type": "nav_item", "label": "VLANs", "map_to": "view_vlans", "client_requirement": "client_is_administrator+" }, + { "type": "nav_item", "label": "General", "map_to": "view_general", "client_requirement": "client_is_administrator+" }, + { "type": "nav_item", "label": "Network Interfaces", "map_to": "view_network_interfaces", "client_requirement": "client_is_administrator+" }, + { "type": "nav_item", "label": "Upstream DNS", "map_to": "view_upstream_dns", "client_requirement": "client_is_administrator+" }, + { "type": "nav_item", "label": "VLANs", "map_to": "view_vlans", "client_requirement": "client_is_administrator+" }, { "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "view_inter_vlan", "client_requirement": "client_is_administrator+" }, { "type": "nav_item", "label": "DNS Server", "map_to": "view_dns_server", "client_requirement": "client_is_administrator+" }, { "type": "nav_item", "label": "Port Forwarding", "map_to": "view_port_forwarding", "client_requirement": "client_is_administrator+" }, diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index 64699a1..125209f 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -492,205 +492,6 @@ } ] }, - { - "type": "card", - "label": "Network Interfaces", - "client_requirement": "client_is_administrator+", - "items": [ - { - "type": "form", - "action": "/action/general_cardnetworkinterface_save", - "method": "post", - "items": [ - { - "type": "field", - "label": "WAN Interface", - "name": "wan_interface", - "input_type": "interface_picker", - "value": "%GENERAL_WAN_INTERFACE%", - "data": "%NETWORK_INTERFACE_DATA_JSON%" - }, - { - "type": "field", - "label": "LAN Interface", - "name": "lan_interface", - "input_type": "interface_picker", - "value": "%GENERAL_LAN_INTERFACE%", - "data": "%NETWORK_INTERFACE_DATA_JSON%" - }, - { - "type": "button_row", - "items": [ - { - "type": "button_primary", - "action": "/action/general_cardnetworkinterface_save", - "method": "post", - "text": "Save" - }, - { - "type": "button_cancel", - "text": "Cancel" - } - ] - } - ] - } - ] - }, - { - "type": "card", - "id": "iface-config-card", - "label": "Interface Configuration", - "hidden": true, - "client_requirement": "client_is_administrator+", - "items": [ - { - "type": "form", - "action": "/action/apply_iface_config", - "method": "post", - "items": [ - { - "type": "hidden", - "name": "original_mtu", - "value": "" - }, - { - "type": "hidden", - "name": "original_mac", - "value": "" - }, - { - "type": "field_row", - "cols": 3, - "items": [ - { - "type": "field", - "label": "Interface", - "name": "iface", - "input_type": "text", - "readonly": true, - "value": "" - }, - { - "type": "field", - "label": "MTU", - "name": "mtu", - "input_type": "select", - "value": "", - "options": [ - { - "label": "576", - "value": "576" - }, - { - "label": "1280", - "value": "1280" - }, - { - "label": "1492", - "value": "1492" - }, - { - "label": "1500", - "value": "1500" - }, - { - "label": "4096", - "value": "4096" - }, - { - "label": "9000", - "value": "9000" - } - ] - }, - { - "type": "field", - "label": "MAC Address", - "name": "mac", - "input_type": "text", - "validate": "mac", - "value": "" - } - ] - }, - { - "type": "button_row", - "items": [ - { - "type": "button_primary", - "action": "/action/apply_iface_config", - "method": "post", - "text": "Apply" - }, - { - "type": "button_secondary", - "action": "#", - "text": "Cancel", - "class": "iface-config-cancel" - } - ] - } - ] - } - ] - }, - { - "type": "card", - "label": "Upstream DNS", - "client_requirement": "client_is_administrator+", - "items": [ - { - "type": "form", - "action": "/action/general_cardupstreamdns_save", - "method": "post", - "items": [ - { - "type": "field", - "label": "Strict Order", - "name": "strict_order", - "input_type": "checkbox", - "value": "%DNS_STRICT_ORDER%", - "hint": "Query DNS providers in list order rather than in parallel." - }, - { - "type": "field", - "label": "Cache Size", - "name": "cache_size", - "input_type": "number", - "value": "%DNS_CACHE_SIZE%", - "min": 0, - "hint": "Max DNS responses to cache per instance. Set to 0 to disable caching." - }, - { - "type": "editable_list", - "label": "DNS Providers", - "name": "upstream_servers", - "item_placeholder": "e.g. 1.1.1.1", - "add_label": "Add Provider", - "validate": "ip", - "hint": "DNS resolvers queried for external hostnames. Supports IPv4 and IPv6.", - "items": "%DNS_UPSTREAM_SERVERS_JSON%" - }, - { - "type": "button_row", - "items": [ - { - "type": "button_primary", - "action": "/action/general_cardupstreamdns_save", - "method": "post", - "text": "Save" - }, - { - "type": "button_cancel", - "text": "Cancel" - } - ] - } - ] - } - ] - }, { "type": "card", "label": "Logging", @@ -788,6 +589,243 @@ } ] }, + { + "id": "view_network_interfaces", + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "page_header", + "items": [ + { + "type": "h1", + "text": "Network Interfaces" + }, + { + "type": "p", + "text": "WAN/LAN interface assignments and per-interface settings." + } + ] + }, + { + "type": "card", + "label": "Network Interfaces", + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "form", + "action": "/action/networkinterfaces_cardnetworkinterface_save", + "method": "post", + "items": [ + { + "type": "field", + "label": "WAN Interface", + "name": "wan_interface", + "input_type": "interface_picker", + "value": "%GENERAL_WAN_INTERFACE%", + "data": "%NETWORK_INTERFACE_DATA_JSON%" + }, + { + "type": "field", + "label": "LAN Interface", + "name": "lan_interface", + "input_type": "interface_picker", + "value": "%GENERAL_LAN_INTERFACE%", + "data": "%NETWORK_INTERFACE_DATA_JSON%" + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "action": "/action/networkinterfaces_cardnetworkinterface_save", + "method": "post", + "text": "Save" + }, + { + "type": "button_cancel", + "text": "Cancel" + } + ] + } + ] + } + ] + }, + { + "type": "card", + "id": "iface-config-card", + "label": "Interface Configuration", + "hidden": true, + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "form", + "action": "/action/networkinterfaces_cardinterfaceconfiguration_apply", + "method": "post", + "items": [ + { + "type": "hidden", + "name": "original_mtu", + "value": "" + }, + { + "type": "hidden", + "name": "original_mac", + "value": "" + }, + { + "type": "field_row", + "cols": 3, + "items": [ + { + "type": "field", + "label": "Interface", + "name": "iface", + "input_type": "text", + "readonly": true, + "value": "" + }, + { + "type": "field", + "label": "MTU", + "name": "mtu", + "input_type": "select", + "value": "", + "options": [ + { + "label": "576", + "value": "576" + }, + { + "label": "1280", + "value": "1280" + }, + { + "label": "1492", + "value": "1492" + }, + { + "label": "1500", + "value": "1500" + }, + { + "label": "4096", + "value": "4096" + }, + { + "label": "9000", + "value": "9000" + } + ] + }, + { + "type": "field", + "label": "MAC Address", + "name": "mac", + "input_type": "text", + "validate": "mac", + "value": "" + } + ] + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "action": "/action/networkinterfaces_cardinterfaceconfiguration_apply", + "method": "post", + "text": "Apply" + }, + { + "type": "button_secondary", + "action": "#", + "text": "Cancel", + "class": "iface-config-cancel" + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "view_upstream_dns", + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "page_header", + "items": [ + { + "type": "h1", + "text": "Upstream DNS" + }, + { + "type": "p", + "text": "Upstream resolvers and caching behaviour for dnsmasq." + } + ] + }, + { + "type": "card", + "label": "Upstream DNS", + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "form", + "action": "/action/upstreamdns_cardupstreamdns_save", + "method": "post", + "items": [ + { + "type": "field", + "label": "Strict Order", + "name": "strict_order", + "input_type": "checkbox", + "value": "%DNS_STRICT_ORDER%", + "hint": "Query DNS providers in list order rather than in parallel." + }, + { + "type": "field", + "label": "Cache Size", + "name": "cache_size", + "input_type": "number", + "value": "%DNS_CACHE_SIZE%", + "min": 0, + "hint": "Max DNS responses to cache per instance. Set to 0 to disable caching." + }, + { + "type": "editable_list", + "label": "DNS Providers", + "name": "upstream_servers", + "item_placeholder": "e.g. 1.1.1.1", + "add_label": "Add Provider", + "validate": "ip", + "hint": "DNS resolvers queried for external hostnames. Supports IPv4 and IPv6.", + "items": "%DNS_UPSTREAM_SERVERS_JSON%" + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "action": "/action/upstreamdns_cardupstreamdns_save", + "method": "post", + "text": "Save" + }, + { + "type": "button_cancel", + "text": "Cancel" + } + ] + } + ] + } + ] + } + ] + }, { "id": "view_banned_ips", "client_requirement": "client_is_viewer+", @@ -1051,11 +1089,11 @@ "items": [ { "type": "h1", - "text": "DNS Server" + "text": "DNS Blocklists" }, { "type": "p", - "text": "Blocklist sources and DNS server settings." + "text": "DNS level blocking via dnsmasq." } ] }, @@ -1086,7 +1124,7 @@ "row_actions": [ { "client_requirement": "client_is_administrator+", - "action": "/action/dnsserver_tableblocklist_rowedit", + "action": "/action/dnsblocklists_tableblocklist_rowedit", "method": "inline_edit", "text": "Edit", "class": "btn-ghost btn-sm", @@ -1114,7 +1152,7 @@ }, { "client_requirement": "client_is_administrator+", - "action": "/action/dnsserver_tableblocklists_rowdelete", + "action": "/action/dnsblocklists_tableblocklists_rowdelete", "method": "post", "text": "Delete", "class": "btn-danger btn-sm" @@ -1129,7 +1167,7 @@ "items": [ { "type": "form", - "action": "/action/dnsserver_cardaddblocklist_add", + "action": "/action/dnsblocklists_cardaddblocklist_add", "method": "post", "items": [ { @@ -1167,7 +1205,7 @@ "items": [ { "type": "button_primary", - "action": "/action/dnsserver_cardaddblocklist_add", + "action": "/action/dnsblocklists_cardaddblocklist_add", "method": "post", "text": "Add Blocklist" }, @@ -1191,19 +1229,12 @@ "html": "%BLOCKLIST_STATS_HTML%" }, { - "type": "button_row", - "items": [ - { - "type": "button_secondary", - "action": "/action/dnsserver_cardblocklistrefresh_refresh", - "method": "post", - "text": "Refresh All Now" - } - ] + "type": "raw_html", + "html": "
" }, { "type": "form", - "action": "/action/dnsserver_cardblocklistrefresh_save", + "action": "/action/dnsblocklists_cardblocklistrefresh_save", "method": "post", "items": [ { @@ -1220,7 +1251,7 @@ "items": [ { "type": "button_primary", - "action": "/action/dnsserver_cardblocklistrefresh_save", + "action": "/action/dnsblocklists_cardblocklistrefresh_save", "method": "post", "text": "Save" }, @@ -1231,6 +1262,17 @@ ] } ] + }, + { + "type": "button_row", + "items": [ + { + "type": "button_secondary", + "action": "/action/dnsblocklists_cardblocklistrefresh_refresh", + "method": "post", + "text": "Refresh All Now" + } + ] } ] }