diff --git a/docker/routlin-dash/app/action_apply_ddns_ip_check.py b/docker/routlin-dash/app/action_apply_ddns_ip_check.py
new file mode 100644
index 0000000..2bfdc53
--- /dev/null
+++ b/docker/routlin-dash/app/action_apply_ddns_ip_check.py
@@ -0,0 +1,31 @@
+from flask import Blueprint, request, redirect, flash
+from auth import require_level
+from config_utils import load_core, save_core, verify_core_hash
+
+bp = Blueprint('action_apply_ddns_ip_check', __name__)
+
+VIEW = '/view/view_ddns'
+
+
+@bp.route('/action/ddns_ip_check_save', methods=['POST'])
+@require_level('administrator')
+def ddns_ip_check_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)
+
+ http_services = [u.strip() for u in request.form.getlist('http_services') if u.strip()]
+ dig_services = [u.strip() for u in request.form.getlist('dig_services') if u.strip()]
+
+ if not http_services and not dig_services:
+ flash('At least one IP check service is required.', 'error')
+ return redirect(VIEW)
+
+ services = [{'type': 'http', 'url': u} for u in http_services]
+ services += [{'type': 'dig', 'url': u} for u in dig_services]
+
+ core = load_core()
+ core.setdefault('ddns', {})['ip_check_services'] = services
+ save_core(core)
+ flash('IP check services saved.', 'success')
+ return redirect(VIEW)
diff --git a/docker/routlin-dash/app/main.py b/docker/routlin-dash/app/main.py
index 364a081..7ade6cb 100644
--- a/docker/routlin-dash/app/main.py
+++ b/docker/routlin-dash/app/main.py
@@ -23,6 +23,7 @@ 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_ddns_ip_check import bp as action_apply_ddns_ip_check_bp
from api_apply_status import bp as api_apply_status_bp
app = Flask(__name__)
@@ -50,6 +51,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_ddns_ip_check_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 1a79481..2cdd096 100644
--- a/docker/routlin-dash/app/view_page.py
+++ b/docker/routlin-dash/app/view_page.py
@@ -324,10 +324,10 @@ def _config_datasource(name):
if ptype == 'noip':
row['credentials'] = (f'
'
f'U: {e(p.get("username", "-"))} '
- f'P: •••
')
+ f'P: ••••••')
elif ptype in ('cloudflare', 'duckdns'):
tok = p.get('api_token', '')
- row['credentials'] = f'API Token: {e(tok[:24])}...' if tok else '(not set)'
+ row['credentials'] = f'API Token: {e(tok[:20])}...' if tok else '(not set)'
else:
row['credentials'] = '-'
row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', [])))
@@ -686,6 +686,13 @@ def collect_tokens():
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))
+ _ip_check = ddns.get('ip_check_services', [])
+ _http_svc = [s['url'] for s in _ip_check if s.get('type') == 'http']
+ _dig_svc = [s['url'] for s in _ip_check if s.get('type') == 'dig']
+ tokens['STAT_IP_CHECK_TOTAL'] = str(len(_ip_check))
+ tokens['STAT_IP_CHECK_SUB'] = f'{len(_http_svc)} http and {len(_dig_svc)} dig'
+ tokens['IP_CHECK_HTTP_JSON'] = json.dumps(_http_svc)
+ tokens['IP_CHECK_DIG_JSON'] = json.dumps(_dig_svc)
_ddns_labels = {'noip': 'No-IP', 'cloudflare': 'Cloudflare', 'duckdns': 'DuckDNS'}
tokens['DDNS_PROVIDER_OPTIONS'] = json.dumps([
{'value': p, 'label': _ddns_labels.get(p, p.title())}
@@ -872,8 +879,9 @@ def _render_item(item, tokens, inherited_req=None):
return f'{text}'
if t == 'button_cancel':
- text = e(apply_tokens(item.get('text', 'Cancel'), tokens))
- return f''
+ text = e(apply_tokens(item.get('text', 'Cancel'), tokens))
+ extra_cls = (' ' + item['class']) if item.get('class') else ''
+ return f''
if t == 'page_header':
return f'