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'')
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",