From c8607ba8c5cc087671069b92effce17fc9df9bfe Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sun, 24 May 2026 02:28:52 -0400 Subject: [PATCH] Development --- .../app/action_apply_ddns_providers.py | 41 ++++++++++- docker/routlin-dash/app/view_page.py | 68 ++++++++++++++++--- docker/routlin-dash/data/page_content.json | 43 +++++++++++- 3 files changed, 139 insertions(+), 13 deletions(-) diff --git a/docker/routlin-dash/app/action_apply_ddns_providers.py b/docker/routlin-dash/app/action_apply_ddns_providers.py index e2650b6..661fd9a 100644 --- a/docker/routlin-dash/app/action_apply_ddns_providers.py +++ b/docker/routlin-dash/app/action_apply_ddns_providers.py @@ -1,6 +1,7 @@ +import re from flask import Blueprint, request, redirect, flash from auth import require_level -from config_utils import load_core, save_core +from config_utils import load_core, save_core, verify_core_hash, queued_msg import sanitize import validation as validate @@ -87,6 +88,44 @@ def edit_ddns_provider(): return redirect(VIEW) +@bp.route('/action/ddns_cardlog_save', methods=['POST']) +@require_level('administrator') +def ddns_cardlog_save(): + log_max_kb = validate.int_range(request.form.get('log_max_kb', '').strip(), 64, None) + if log_max_kb is None: + flash('Max Log Size must be a number >= 64.', 'error') + return redirect(VIEW) + log_errors_only = 'log_errors_only' in request.form + 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('ddns', {}).setdefault('general', {}).update({ + 'log_max_kb': log_max_kb, + 'log_errors_only': log_errors_only, + }) + save_core(core) + flash('DDNS log settings saved.', 'success') + return redirect(VIEW) + + +@bp.route('/action/ddns_cardinterval_save', methods=['POST']) +@require_level('administrator') +def ddns_cardinterval_save(): + timer_interval = request.form.get('timer_interval', '').strip() + if not re.match(r'^\d+[mhd]$', timer_interval): + flash('Invalid interval. Use a number followed by m, h, or d (e.g. 10m, 1h).', '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('ddns', {}).setdefault('general', {})['timer_interval'] = timer_interval + save_core(core) + flash(queued_msg('core apply'), 'success') + return redirect(VIEW) + + @bp.route('/action/delete_ddns_provider', methods=['POST']) @require_level('administrator') def delete_ddns_provider(): diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 4aa1401..4ad1a70 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -459,7 +459,8 @@ DDNS_LOG_MAX = 50 def _ddns_log_tail(): log_path = f'{CONFIGS_DIR}/ddns.log' try: - size_kb = os.path.getsize(log_path) / 1024 + log_max_kb = _load_ddns().get('general', {}).get('log_max_kb', 1024) + size_kb = os.path.getsize(log_path) / 1024 with open(log_path) as f: lines = f.readlines() if not lines: @@ -468,10 +469,11 @@ def _ddns_log_tail(): tail = lines[-DDNS_LOG_MAX:] shown = len(tail) hidden = total - shown - size_str = f'{size_kb:.1f} KB' - left = f'Showing last {shown} lines ({hidden} lines not shown)' if hidden > 0 else f'Showing {shown} lines' + pct = min(100, round(size_kb / log_max_kb * 100)) if log_max_kb else 0 + left = f'Showing last {shown} lines ({hidden} lines not shown)' if hidden > 0 else f'Showing {shown} lines' + right = f'Log file size: {size_kb:.1f} KB ({pct}% of max)' summary = (f'
' - f'{left}Log file size: {size_str}
') + f'{left}{right}') return ''.join(tail).strip(), summary except FileNotFoundError: return '(log file not found)', '' @@ -570,7 +572,7 @@ def _ddns_last_checked(): return f'Last checked: {_relative_time(dt.timestamp())}' except Exception: pass - return '' + return 'Last checked: ---' def _vpn_info(): for vlan in _load_core().get('vlans', []): @@ -676,7 +678,10 @@ def collect_tokens(): tokens['BLOCKLIST_STATS_HTML'] = _blocklist_stats_html(core) ddns = _load_ddns() - tokens['DDNS_TIMER_INTERVAL'] = ddns.get('general', {}).get('timer_interval', '-') + ddns_gen = ddns.get('general', {}) + tokens['DDNS_TIMER_INTERVAL'] = ddns_gen.get('timer_interval', '-') + tokens['DDNS_GEN_LOG_MAX_KB'] = str(ddns_gen.get('log_max_kb', 1024)) + tokens['DDNS_GEN_LOG_ERRORS_ONLY'] = 'true' if ddns_gen.get('log_errors_only') else 'false' enabled_p = [p for p in ddns.get('providers', []) if p.get('enabled', True)] tokens['STAT_DDNS_PROVIDER_COUNT'] = str(len(enabled_p)) _ddns_labels = {'noip': 'No-IP', 'cloudflare': 'Cloudflare', 'duckdns': 'DuckDNS'} @@ -827,6 +832,9 @@ def _render_item(item, tokens, inherited_req=None): if t == 'h1': return f'

{e(apply_tokens(item.get("text", ""), tokens))}

' + if t == 'hr': + return '
' + if t == 'p': text = e(apply_tokens(item.get('text', ''), tokens)) link = item.get('link') @@ -880,11 +888,33 @@ def _render_item(item, tokens, inherited_req=None): return f'
{render_items(item.get("items", []), tokens, req)}
' if t == 'stat_card': - label = e(apply_tokens(item.get('label', ''), tokens)) - value = e(apply_tokens(item.get('value', ''), tokens)) - sub = e(apply_tokens(item.get('sub', ''), tokens)) - variant = item.get('variant', '') - cls = f'stat-card{(" stat-card-" + variant) if variant else ""}' + label = e(apply_tokens(item.get('label', ''), tokens)) + raw_value = apply_tokens(item.get('value', ''), tokens) + value = e(raw_value) + sub = e(apply_tokens(item.get('sub', ''), tokens)) + variant = item.get('variant', '') + cls = f'stat-card{(" stat-card-" + variant) if variant else ""}' + edit_action = item.get('edit_action', '') + edit_field = item.get('edit_field', '') + if edit_action and edit_field: + return ( + f'
' + f'
{label}
' + f'
' + f'{value}' + f'' + f'
' + f'' + f'
{sub}
' + f'
' + ) return (f'
' f'
{label}
' f'
{value}
' @@ -2547,6 +2577,22 @@ function startApplyPoller(uuid, bar, mine) { document.querySelectorAll('.pre-block[data-scroll-bottom]').forEach(function(el) { el.scrollTop = el.scrollHeight; }); +(function() { + document.querySelectorAll('.stat-card-edit-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + var card = btn.closest('.stat-card-editable'); + card.querySelector('.stat-card-view').style.display = 'none'; + card.querySelector('.stat-card-edit-form').style.display = ''; + }); + }); + document.querySelectorAll('.stat-card-cancel-btn').forEach(function(btn) { + btn.addEventListener('click', function() { + var card = btn.closest('.stat-card-editable'); + card.querySelector('.stat-card-view').style.display = ''; + card.querySelector('.stat-card-edit-form').style.display = 'none'; + }); + }); +})(); """ diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index 1370ff7..dd488ba 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -319,7 +319,9 @@ "type": "stat_card", "label": "Check Interval", "value": "%DDNS_TIMER_INTERVAL%", - "sub": "%STAT_PUBLIC_IP_LAST_CHECKED%" + "sub": "%STAT_PUBLIC_IP_LAST_CHECKED%", + "edit_action": "/action/ddns_cardinterval_save", + "edit_field": "timer_interval" }, { "type": "stat_card", @@ -476,6 +478,45 @@ "text": "Clear Log" } ] + }, + { + "type": "hr" + }, + { + "type": "form", + "action": "/action/ddns_cardlog_save", + "method": "post", + "items": [ + { + "type": "field", + "label": "Max Log Size (KB)", + "name": "log_max_kb", + "input_type": "number", + "value": "%DDNS_GEN_LOG_MAX_KB%", + "min": "64" + }, + { + "type": "field", + "label": "Log errors only", + "name": "log_errors_only", + "input_type": "checkbox", + "checkbox_label": "Only record errors to log", + "value": "%DDNS_GEN_LOG_ERRORS_ONLY%" + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "text": "Save" + }, + { + "type": "button_cancel", + "text": "Cancel" + } + ] + } + ] } ] }