From 226a2e2e0656699167db68dcb0166227ce3cb65d Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sat, 23 May 2026 00:27:37 -0400 Subject: [PATCH] Development --- .../routlin-dash/app/action_apply_general.py | 44 --- .../app/action_apply_interface.py | 65 ----- .../routlin-dash/app/action_apply_pending.py | 26 -- .../app/action_apply_upstream_dns.py | 59 ---- ...pply_blocklists.py => action_dnsserver.py} | 83 +++--- docker/routlin-dash/app/action_general.py | 194 +++++++++++++ docker/routlin-dash/app/main.py | 14 +- docker/routlin-dash/app/view_page.py | 38 ++- docker/routlin-dash/data/navbar_content.json | 25 +- docker/routlin-dash/data/page_content.json | 264 ++++++++++-------- 10 files changed, 444 insertions(+), 368 deletions(-) delete mode 100644 docker/routlin-dash/app/action_apply_general.py delete mode 100644 docker/routlin-dash/app/action_apply_interface.py delete mode 100644 docker/routlin-dash/app/action_apply_pending.py delete mode 100644 docker/routlin-dash/app/action_apply_upstream_dns.py rename docker/routlin-dash/app/{action_apply_blocklists.py => action_dnsserver.py} (77%) create mode 100644 docker/routlin-dash/app/action_general.py diff --git a/docker/routlin-dash/app/action_apply_general.py b/docker/routlin-dash/app/action_apply_general.py deleted file mode 100644 index 2be86a0..0000000 --- a/docker/routlin-dash/app/action_apply_general.py +++ /dev/null @@ -1,44 +0,0 @@ -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_apply_general', __name__) - - -@bp.route('/action/apply_general', methods=['POST']) -@require_level('administrator') -def apply_general(): - log_max_kb_raw = request.form.get('log_max_kb', '').strip() - log_errors_only = 'log_errors_only' in request.form - dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form - daily_execute_time = sanitize.time_24h(request.form.get('daily_execute_time_24hr_local', '')) - apply_on_save = 'apply_on_save' in request.form - - log_max_kb = validate.int_range(log_max_kb_raw, 64, None) - if log_max_kb is None: - flash('Max Log Size must be a number >= 64.', 'error') - return redirect('/view/view_general') - - 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/view_general') - - core = load_core() - core.setdefault('general', {}).update({ - 'log_max_kb': log_max_kb, - 'log_errors_only': log_errors_only, - 'dnsmasq_log_queries': dnsmasq_log_queries, - 'daily_execute_time_24hr_local': daily_execute_time, - 'apply_on_save': apply_on_save, - }) - errors = validate.validate_config(core) - if errors: - for msg in errors: - flash(msg, 'error') - return redirect('/view/view_general') - save_core(core) - - flash(queued_msg('core apply'), 'success') - return redirect('/view/view_general') diff --git a/docker/routlin-dash/app/action_apply_interface.py b/docker/routlin-dash/app/action_apply_interface.py deleted file mode 100644 index 38ccf63..0000000 --- a/docker/routlin-dash/app/action_apply_interface.py +++ /dev/null @@ -1,65 +0,0 @@ -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 -import sanitize -import validation as validate - -bp = Blueprint('action_apply_interface', __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/apply_interface', methods=['POST']) -@require_level('administrator') -def apply_interface(): - 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) diff --git a/docker/routlin-dash/app/action_apply_pending.py b/docker/routlin-dash/app/action_apply_pending.py deleted file mode 100644 index c08f1cc..0000000 --- a/docker/routlin-dash/app/action_apply_pending.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask import Blueprint, redirect, flash -from auth import require_level -from config_utils import (flush_pending_to_queue, get_dashboard_pending, - _is_locked, _format_timing, _seconds_until_next_run) - -bp = Blueprint('action_apply_pending', __name__) - - -@bp.route('/action/apply_pending', methods=['POST']) -@require_level('administrator') -def apply_pending(): - items = get_dashboard_pending() - if not items: - flash('No pending changes to apply.', 'info') - return redirect('/view/view_general') - flush_pending_to_queue() - if _is_locked(): - msg = 'Changes queued. They are being applied now.' - else: - timing = _format_timing(_seconds_until_next_run()) - if timing: - msg = f'Changes queued. They will be applied {timing}.' - else: - msg = 'Changes queued. The processing service is not running.' - flash(msg, 'success') - return redirect('/view/view_general') diff --git a/docker/routlin-dash/app/action_apply_upstream_dns.py b/docker/routlin-dash/app/action_apply_upstream_dns.py deleted file mode 100644 index 757205f..0000000 --- a/docker/routlin-dash/app/action_apply_upstream_dns.py +++ /dev/null @@ -1,59 +0,0 @@ -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_apply_upstream_dns', __name__) - - -@bp.route('/action/apply_upstream_dns', methods=['POST']) -@require_level('administrator') -def apply_upstream_dns(): - 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/view_upstream_dns') - - 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/view_upstream_dns') - 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/view_upstream_dns') - - 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/view_upstream_dns') - - 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/view_upstream_dns') - - 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/view_upstream_dns') - save_core(core) - flash(queued_msg('core apply'), 'success') - return redirect('/view/view_upstream_dns') diff --git a/docker/routlin-dash/app/action_apply_blocklists.py b/docker/routlin-dash/app/action_dnsserver.py similarity index 77% rename from docker/routlin-dash/app/action_apply_blocklists.py rename to docker/routlin-dash/app/action_dnsserver.py index 85b79a1..fef8fd1 100644 --- a/docker/routlin-dash/app/action_apply_blocklists.py +++ b/docker/routlin-dash/app/action_dnsserver.py @@ -5,9 +5,9 @@ import re import sanitize import validation as validate -bp = Blueprint('action_apply_blocklists', __name__) +bp = Blueprint('action_dnsserver', __name__) -VIEW = '/view/view_blocklists' +VIEW = '/view/view_dns_server' _VALID_FORMATS_STR = ', '.join(sorted(validate.VALID_BLOCKLIST_FORMATS)) @@ -32,7 +32,6 @@ def _save_as_from_name(name): def _parse_fields(): - """Parse and validate add/edit form fields. Returns (fields_dict, None) or (None, already_flashed).""" name = sanitize.name(request.form.get('name', '')) description = sanitize.description(request.form.get('description', '')) fmt = sanitize.filtervalue(request.form.get('format', ''), validate.VALID_BLOCKLIST_FORMATS) @@ -52,30 +51,24 @@ def _parse_fields(): return {'name': name, 'description': description, 'format': fmt, 'url': url}, None -@bp.route('/action/add_blocklist', methods=['POST']) +@bp.route('/action/dnsserver_tableblocklists_rowdelete', methods=['POST']) @require_level('administrator') -def add_blocklist(): - fields, err = _parse_fields() - if err: +def dnsserver_tableblocklists_rowdelete(): + idx = _row_index() + if idx is None: + flash('Invalid request.', 'error') return redirect(VIEW) if not _hash_ok(): return redirect(VIEW) - core = load_core() - blocklists = core.setdefault('blocklists', []) - - if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists): - flash('The configuration has not been saved because a blocklist with that name already exists.', 'error') + core = load_core() + items = core.get('blocklists', []) + if idx < 0 or idx >= len(items): + flash('Entry not found.', 'error') return redirect(VIEW) - blocklists.append({ - 'name': fields['name'], - 'description': fields['description'], - 'format': fields['format'], - 'url': fields['url'], - 'save_as': _save_as_from_name(fields['name']), - }) + items.pop(idx) errors = validate.validate_config(core) if errors: for msg in errors: @@ -87,10 +80,9 @@ def add_blocklist(): return redirect(VIEW) - -@bp.route('/action/edit_blocklist', methods=['POST']) +@bp.route('/action/dnsserver_tableblocklist_rowedit', methods=['POST']) @require_level('administrator') -def edit_blocklist(): +def dnsserver_tableblocklist_rowedit(): idx = _row_index() if idx is None: flash('Invalid request.', 'error') @@ -126,24 +118,30 @@ def edit_blocklist(): return redirect(VIEW) -@bp.route('/action/delete_blocklist', methods=['POST']) +@bp.route('/action/dnsserver_cardaddblocklist_add', methods=['POST']) @require_level('administrator') -def delete_blocklist(): - idx = _row_index() - if idx is None: - flash('Invalid request.', 'error') +def dnsserver_cardaddblocklist_add(): + fields, err = _parse_fields() + if err: return redirect(VIEW) if not _hash_ok(): return redirect(VIEW) - core = load_core() - items = core.get('blocklists', []) - if idx < 0 or idx >= len(items): - flash('Entry not found.', 'error') + core = load_core() + blocklists = core.setdefault('blocklists', []) + + if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists): + flash('The configuration has not been saved because a blocklist with that name already exists.', 'error') return redirect(VIEW) - removed = items.pop(idx) + blocklists.append({ + 'name': fields['name'], + 'description': fields['description'], + 'format': fields['format'], + 'url': fields['url'], + 'save_as': _save_as_from_name(fields['name']), + }) errors = validate.validate_config(core) if errors: for msg in errors: @@ -155,8 +153,25 @@ def delete_blocklist(): return redirect(VIEW) -@bp.route('/action/update_blocklists', methods=['POST']) +@bp.route('/action/dnsserver_cardblocklistrefresh_save', methods=['POST']) @require_level('administrator') -def update_blocklists(): +def dnsserver_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', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(VIEW) + + core = load_core() + core.setdefault('general', {})['daily_execute_time_24hr_local'] = daily_execute_time + save_core(core) + + flash(queued_msg('core apply'), 'success') + return redirect(VIEW) + + +@bp.route('/action/dnsserver_cardblocklistrefresh_refresh', methods=['POST']) +@require_level('administrator') +def dnsserver_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 new file mode 100644 index 0000000..4bc7a4c --- /dev/null +++ b/docker/routlin-dash/app/action_general.py @@ -0,0 +1,194 @@ +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') +def general_cardlogging_save(): + log_max_kb_raw = request.form.get('log_max_kb', '').strip() + log_errors_only = 'log_errors_only' in request.form + dnsmasq_log_queries = 'dnsmasq_log_queries' in request.form + + log_max_kb = validate.int_range(log_max_kb_raw, 64, None) + if log_max_kb is None: + flash('Max Log Size must be a number >= 64.', '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() + core.setdefault('general', {}).update({ + 'log_max_kb': log_max_kb, + 'log_errors_only': log_errors_only, + 'dnsmasq_log_queries': dnsmasq_log_queries, + }) + 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_cardpendingchanges_save', methods=['POST']) +@require_level('administrator') +def general_cardpendingchanges_save(): + 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() + core.setdefault('general', {})['apply_on_save'] = 'apply_on_save' in request.form + save_core(core) + + flash(queued_msg('core apply'), 'success') + return redirect(_VIEW) + + +@bp.route('/action/general_cardpendingchanges_applyselected', methods=['POST']) +@require_level('administrator') +def general_cardpendingchanges_applyselected(): + items = get_dashboard_pending() + if not items: + flash('No pending changes to apply.', 'info') + return redirect(_VIEW) + flush_pending_to_queue() + if _is_locked(): + msg = 'Changes queued. They are being applied now.' + else: + timing = _format_timing(_seconds_until_next_run()) + if timing: + msg = f'Changes queued. They will be applied {timing}.' + else: + msg = 'Changes queued. The processing service is not running.' + flash(msg, 'success') + return redirect(_VIEW) + + +@bp.route('/action/general_cardpendingchanges_deleteselected', methods=['POST']) +@require_level('administrator') +def general_cardpendingchanges_deleteselected(): + flash('Not yet implemented.', 'info') + return redirect(_VIEW) diff --git a/docker/routlin-dash/app/main.py b/docker/routlin-dash/app/main.py index 8b5eeea..6236bb9 100644 --- a/docker/routlin-dash/app/main.py +++ b/docker/routlin-dash/app/main.py @@ -1,13 +1,12 @@ import os, json, sys from flask import Flask from view_page import bp as view_page_bp -from action_apply_general import bp as action_apply_general_bp -from action_apply_upstream_dns import bp as action_apply_upstream_dns_bp +from action_general import bp as action_general_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_apply_blocklists import bp as action_apply_blocklists_bp +from action_dnsserver import bp as action_dnsserver_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 @@ -22,21 +21,18 @@ 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_interface import bp as action_apply_interface_bp from action_apply_iface_config import bp as action_apply_iface_config_bp -from action_apply_pending import bp as action_apply_pending_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_apply_general_bp) -app.register_blueprint(action_apply_upstream_dns_bp) +app.register_blueprint(action_general_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_apply_blocklists_bp) +app.register_blueprint(action_dnsserver_bp) app.register_blueprint(action_apply_vlans_bp) app.register_blueprint(action_apply_inter_vlan_bp) app.register_blueprint(action_apply_port_forwarding_bp) @@ -51,9 +47,7 @@ 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_interface_bp) app.register_blueprint(action_apply_iface_config_bp) -app.register_blueprint(action_apply_pending_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 37fedcd..d71f8d9 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -398,6 +398,41 @@ def _bl_last_update(): except Exception: return '-' +def _blocklist_stats_html(core): + bl_dir = f'{CONFIGS_DIR}/blocklists' + rows = '' + for bl in core.get('blocklists', []): + name = e(bl.get('name', '')) + save_as = bl.get('save_as', '') + bl_path = f'{bl_dir}/{save_as}' if save_as else '' + try: + with open(bl_path) as f: + entries = sum(1 for _ in f) + size_str = _fmt_bytes(os.path.getsize(bl_path)) + last_refreshed = _fmt_timestamp(int(os.path.getmtime(bl_path))) + except Exception: + entries, size_str, last_refreshed = '-', '-', 'Never' + rows += (f'' + f'{name}' + f'{entries}' + f'{size_str}' + f'{e(last_refreshed)}' + f'') + if not rows: + return '' + return ( + '' + '' + '' + '' + '' + '' + '' + f'{rows}' + '
BlocklistEntriesSizeLast Refreshed
' + ) + + def _ddns_log_tail(n=50): log_path = f'{CONFIGS_DIR}/ddns.log' try: @@ -552,7 +587,7 @@ def collect_tokens(): '' f'{rows}' '' - '
' + '' f'' '
' '' @@ -586,6 +621,7 @@ def collect_tokens(): tokens['EXISTING_VLAN_INTERFACES_JSON'] = json.dumps([_resolve_iface(v, core) for v in vlans]) tokens['STAT_BANNED_IP_COUNT'] = str(sum(1 for b in core.get('banned_ips', []) if b.get('enabled', True))) tokens['STAT_BLOCKLIST_COUNT'] = str(len(core.get('blocklists', []))) + tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(core) ddns = _load_ddns() tokens['DDNS_TIMER_INTERVAL'] = ddns.get('general', {}).get('timer_interval', '-') diff --git a/docker/routlin-dash/data/navbar_content.json b/docker/routlin-dash/data/navbar_content.json index 37b7f0b..0eb2b0a 100644 --- a/docker/routlin-dash/data/navbar_content.json +++ b/docker/routlin-dash/data/navbar_content.json @@ -11,17 +11,16 @@ "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": "Inter-VLAN Exceptions","map_to": "view_inter_vlan", "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": "DNS Blocklists", "map_to": "view_blocklists", "client_requirement": "client_is_administrator+" }, - { "type": "nav_item", "label": "Port Forwarding", "map_to": "view_port_forwarding","client_requirement": "client_is_administrator+" }, - { "type": "nav_item", "label": "DHCP", "map_to": "view_dhcp" }, - { "type": "nav_item", "label": "Host Overrides", "map_to": "view_host_overrides", "client_requirement": "client_is_administrator+" }, - { "type": "nav_item", "label": "DDNS", "map_to": "view_ddns" }, - { "type": "nav_item", "label": "VPN", "map_to": "view_vpn" }, - { "type": "nav_item", "label": "Banned IPs", "map_to": "view_banned_ips", "client_requirement": "client_is_administrator+" } + { "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": "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+" }, + { "type": "nav_item", "label": "DHCP", "map_to": "view_dhcp" }, + { "type": "nav_item", "label": "Host Overrides", "map_to": "view_host_overrides", "client_requirement": "client_is_administrator+" }, + { "type": "nav_item", "label": "DDNS", "map_to": "view_ddns" }, + { "type": "nav_item", "label": "VPN", "map_to": "view_vpn" }, + { "type": "nav_item", "label": "Banned IPs", "map_to": "view_banned_ips", "client_requirement": "client_is_administrator+" } ] }, { @@ -30,9 +29,9 @@ "align": "right", "client_requirement": "client_is_viewer+", "items": [ - { "type": "nav_item", "label": "Preferences", "map_to": "view_preferences" }, + { "type": "nav_item", "label": "Preferences", "map_to": "view_preferences" }, { "type": "nav_item", "label": "Manage Accounts", "map_to": "view_manage_accounts", "client_requirement": "client_is_manager+" }, - { "type": "nav_action", "label": "Log Out", "action": "log_out" } + { "type": "nav_action", "label": "Log Out", "action": "log_out" } ] }, { diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index 501947b..fab4312 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -499,7 +499,7 @@ "items": [ { "type": "form", - "action": "/action/apply_interface", + "action": "/action/general_cardnetworkinterface_save", "method": "post", "items": [ { @@ -524,7 +524,7 @@ { "type": "button_primary", "text": "Save", - "action": "/action/apply_interface", + "action": "/action/general_cardnetworkinterface_save", "method": "post" }, { @@ -537,6 +537,62 @@ } ] }, + { + "type": "card", + "label": "Upstream DNS", + "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", + "items": "%DNS_UPSTREAM_SERVERS_JSON%", + "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." + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "text": "Save", + "action": "/action/general_cardupstreamdns_save", + "method": "post" + }, + { + "type": "button_cancel", + "text": "Cancel" + } + ] + } + ] + } + ], + "client_requirement": "client_is_administrator+" + }, { "type": "card", "id": "iface-config-card", @@ -578,12 +634,30 @@ "input_type": "select", "value": "", "options": [ - {"value": "576", "label": "576"}, - {"value": "1280", "label": "1280"}, - {"value": "1492", "label": "1492"}, - {"value": "1500", "label": "1500"}, - {"value": "4096", "label": "4096"}, - {"value": "9000", "label": "9000"} + { + "value": "576", + "label": "576" + }, + { + "value": "1280", + "label": "1280" + }, + { + "value": "1492", + "label": "1492" + }, + { + "value": "1500", + "label": "1500" + }, + { + "value": "4096", + "label": "4096" + }, + { + "value": "9000", + "label": "9000" + } ] }, { @@ -623,7 +697,7 @@ "items": [ { "type": "form", - "action": "/action/apply_general", + "action": "/action/general_cardlogging_save", "method": "post", "items": [ { @@ -651,22 +725,13 @@ "value": "%GENERAL_DNSMASQ_LOG_QUERIES%", "hint": "Log every DNS query. High volume \u2014 enable for debugging only." }, - { - "type": "field", - "label": "Daily Task Time", - "name": "daily_execute_time_24hr_local", - "input_type": "text", - "value": "%GENERAL_DAILY_EXECUTE_TIME%", - "placeholder": "e.g. 02:30", - "hint": "24-hour local time for the daily blocklist refresh timer." - }, { "type": "button_row", "items": [ { "type": "button_primary", "text": "Save", - "action": "/action/apply_general", + "action": "/action/general_cardlogging_save", "method": "post" }, { @@ -682,12 +747,12 @@ }, { "type": "card", - "label": "Configuration Changes", + "label": "Pending Changes", "client_requirement": "client_is_administrator+", "items": [ { "type": "form", - "action": "/action/apply_general", + "action": "/action/general_cardpendingchanges_save", "method": "post", "items": [ { @@ -704,7 +769,7 @@ { "type": "button_primary", "text": "Save", - "action": "/action/apply_general", + "action": "/action/general_cardpendingchanges_save", "method": "post" }, { @@ -723,81 +788,6 @@ } ] }, - { - "id": "view_upstream_dns", - "client_requirement": "client_is_viewer+", - "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 Settings", - "items": [ - { - "type": "form", - "action": "/action/apply_upstream_dns", - "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", - "items": "%DNS_UPSTREAM_SERVERS_JSON%", - "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." - }, - { - "type": "button_row", - "items": [ - { - "type": "button_primary", - "text": "Save", - "action": "/action/apply_upstream_dns", - "method": "post" - }, - { - "type": "button_cancel", - "text": "Cancel" - } - ] - } - ] - } - ], - "client_requirement": "client_is_administrator+" - } - ] - }, { "id": "view_banned_ips", "client_requirement": "client_is_viewer+", @@ -1053,7 +1043,7 @@ ] }, { - "id": "view_blocklists", + "id": "view_dns_server", "client_requirement": "client_is_viewer+", "items": [ { @@ -1061,11 +1051,11 @@ "items": [ { "type": "h1", - "text": "DNS Blocklists" + "text": "DNS Server" }, { "type": "p", - "text": "Upstream blocklist sources downloaded and merged by the daily systemd timer." + "text": "Blocklist sources and DNS server settings." } ] }, @@ -1093,22 +1083,11 @@ "class": "col-mono" } ], - "toolbar": { - "items": [ - { - "type": "button_secondary", - "text": "Refresh All Now", - "action": "/action/update_blocklists", - "method": "post", - "client_requirement": "client_is_administrator+" - } - ] - }, "row_actions": [ { "text": "Edit", "class": "btn-ghost btn-sm", - "action": "/action/edit_blocklist", + "action": "/action/dnsserver_tableblocklist_rowedit", "method": "inline_edit", "client_requirement": "client_is_administrator+", "fields": [ @@ -1136,7 +1115,7 @@ { "text": "Delete", "class": "btn-danger btn-sm", - "action": "/action/delete_blocklist", + "action": "/action/dnsserver_tableblocklists_rowdelete", "method": "post", "client_requirement": "client_is_administrator+" } @@ -1150,7 +1129,7 @@ "items": [ { "type": "form", - "action": "/action/add_blocklist", + "action": "/action/dnsserver_cardaddblocklist_add", "method": "post", "items": [ { @@ -1189,7 +1168,60 @@ { "type": "button_primary", "text": "Add Blocklist", - "action": "/action/add_blocklist", + "action": "/action/dnsserver_cardaddblocklist_add", + "method": "post" + }, + { + "type": "button_cancel", + "text": "Cancel" + } + ] + } + ] + } + ] + }, + { + "type": "card", + "label": "Blocklist Refresh", + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "raw_html", + "html": "%BLOCKLIST_STATS_HTML%" + }, + { + "type": "button_row", + "items": [ + { + "type": "button_secondary", + "text": "Refresh All Now", + "action": "/action/dnsserver_cardblocklistrefresh_refresh", + "method": "post" + } + ] + }, + { + "type": "form", + "action": "/action/dnsserver_cardblocklistrefresh_save", + "method": "post", + "items": [ + { + "type": "field", + "label": "Daily Task Time", + "name": "daily_execute_time_24hr_local", + "input_type": "text", + "value": "%GENERAL_DAILY_EXECUTE_TIME%", + "placeholder": "e.g. 02:30", + "hint": "24-hour local time for the daily blocklist refresh." + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "text": "Save", + "action": "/action/dnsserver_cardblocklistrefresh_save", "method": "post" }, { @@ -2738,4 +2770,4 @@ ] } ] -} +} \ No newline at end of file