diff --git a/docker/routlin-dash/app/action_apply_ddns_providers.py b/docker/routlin-dash/app/action_apply_ddns_providers.py index 661fd9a..5e03fd9 100644 --- a/docker/routlin-dash/app/action_apply_ddns_providers.py +++ b/docker/routlin-dash/app/action_apply_ddns_providers.py @@ -112,10 +112,15 @@ def ddns_cardlog_save(): @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') + raw = request.form.get('timer_interval', '').strip() + try: + mins = int(raw) + if mins < 1: + raise ValueError + except ValueError: + flash('Interval must be a whole number of minutes >= 1.', 'error') return redirect(VIEW) + timer_interval = f'{mins}m' 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/view_page.py b/docker/routlin-dash/app/view_page.py index 4ad1a70..1a79481 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -680,6 +680,8 @@ def collect_tokens(): ddns = _load_ddns() ddns_gen = ddns.get('general', {}) tokens['DDNS_TIMER_INTERVAL'] = ddns_gen.get('timer_interval', '-') + _interval_secs = _parse_interval_to_seconds(ddns_gen.get('timer_interval', '')) or 600 + tokens['DDNS_TIMER_INTERVAL_MINS'] = str(_interval_secs // 60) 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)] @@ -833,7 +835,7 @@ def _render_item(item, tokens, inherited_req=None): return f'

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

' if t == 'hr': - return '
' + return '
' if t == 'p': text = e(apply_tokens(item.get('text', ''), tokens)) @@ -894,9 +896,19 @@ def _render_item(item, tokens, inherited_req=None): 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', '') + edit_action = item.get('edit_action', '') + edit_field = item.get('edit_field', '') + edit_input_type = item.get('edit_input_type', 'text') + edit_suffix = item.get('edit_suffix', '') + edit_min = item.get('edit_min', '') + edit_raw = apply_tokens(item.get('edit_value', item.get('value', '')), tokens) if edit_action and edit_field: + min_attr = f' min="{e(edit_min)}"' if edit_min else '' + suffix_html = f'{e(edit_suffix)}' if edit_suffix else '' + input_wrap = (f'
' + f'' + f'{suffix_html}
') return ( f'
' f'
{label}
' @@ -906,9 +918,9 @@ def _render_item(item, tokens, inherited_req=None): f'
' f'' @@ -1072,8 +1084,9 @@ def _render_field(item, tokens): checked = 'checked' if value.lower() in ('true', '1', 'yes') else '' cb_label = item.get('checkbox_label') if cb_label: + label_html = f'' if label else '' return (f'
' - f'' + f'{label_html}' f'
') if input_type == 'textarea': rows = item.get('rows', 4) @@ -2578,18 +2597,22 @@ 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'); + document.querySelectorAll('.stat-card-editable').forEach(function(card) { + var form = card.querySelector('.stat-card-edit-form'); + var input = form ? form.querySelector('input[data-original]') : null; + var saveBtn = form ? form.querySelector('button[type="submit"]') : null; + function updateSave() { + if (input && saveBtn) saveBtn.disabled = (input.value === input.dataset.original); + } + if (input) input.addEventListener('input', updateSave); + card.querySelector('.stat-card-edit-btn').addEventListener('click', function() { card.querySelector('.stat-card-view').style.display = 'none'; - card.querySelector('.stat-card-edit-form').style.display = ''; + form.style.display = ''; }); - }); - document.querySelectorAll('.stat-card-cancel-btn').forEach(function(btn) { - btn.addEventListener('click', function() { - var card = btn.closest('.stat-card-editable'); + form && form.querySelector('.stat-card-cancel-btn').addEventListener('click', function() { card.querySelector('.stat-card-view').style.display = ''; - card.querySelector('.stat-card-edit-form').style.display = 'none'; + form.style.display = 'none'; + if (input) { input.value = input.dataset.original; updateSave(); } }); }); })(); diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index dd488ba..591926d 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -321,7 +321,11 @@ "value": "%DDNS_TIMER_INTERVAL%", "sub": "%STAT_PUBLIC_IP_LAST_CHECKED%", "edit_action": "/action/ddns_cardinterval_save", - "edit_field": "timer_interval" + "edit_field": "timer_interval", + "edit_input_type": "number", + "edit_min": "1", + "edit_suffix": "minutes", + "edit_value": "%DDNS_TIMER_INTERVAL_MINS%" }, { "type": "stat_card", @@ -492,12 +496,13 @@ "label": "Max Log Size (KB)", "name": "log_max_kb", "input_type": "number", + "layout": "inline", "value": "%DDNS_GEN_LOG_MAX_KB%", "min": "64" }, { "type": "field", - "label": "Log errors only", + "label": "", "name": "log_errors_only", "input_type": "checkbox", "checkbox_label": "Only record errors to log",