diff --git a/docker/routlin-dash/app/action_apply_vpn.py b/docker/routlin-dash/app/action_apply_vpn.py index ca1fa11..9e98c28 100644 --- a/docker/routlin-dash/app/action_apply_vpn.py +++ b/docker/routlin-dash/app/action_apply_vpn.py @@ -4,7 +4,7 @@ import re from flask import Blueprint, make_response, redirect, flash, request from auth import require_level -from config_utils import load_core, save_core, verify_core_hash, queued_msg, CONFIGS_DIR, PRODUCT_DISPLAY_NAME +from config_utils import load_core, save_core, verify_core_hash, queued_msg, CONFIGS_DIR, WEB_APP_DISPLAY_NAME import sanitize import validation as validate @@ -103,7 +103,7 @@ def _build_client_conf(vlan, peer_name, peer_ip, private_key, server_pubkey): allowed_ips = f'{subnet}/{prefix}' if split_tunnel else '0.0.0.0/0' lines = [ - f'# Generated by {PRODUCT_DISPLAY_NAME}', + f'# Generated by {WEB_APP_DISPLAY_NAME}', '', '[Interface]', f'PrivateKey = {private_key}', diff --git a/docker/routlin-dash/app/action_create_account.py b/docker/routlin-dash/app/action_create_account.py index a27d1a4..ae70e16 100644 --- a/docker/routlin-dash/app/action_create_account.py +++ b/docker/routlin-dash/app/action_create_account.py @@ -3,7 +3,7 @@ import json, os, bcrypt, secrets, smtplib from datetime import datetime, timezone, timedelta from email.message import EmailMessage from auth import require_level -from config_utils import PRODUCT_DISPLAY_NAME, ACCOUNTS_FILE +from config_utils import WEB_APP_DISPLAY_NAME, ACCOUNTS_FILE import sanitize bp = Blueprint('action_create_account', __name__) @@ -30,7 +30,7 @@ def _send_verification_email(to_address, code): raise RuntimeError('SMTP_HOST is not configured.') msg = EmailMessage() - msg['Subject'] = f'{PRODUCT_DISPLAY_NAME} - Email Verification' + msg['Subject'] = f'{WEB_APP_DISPLAY_NAME} - Email Verification' msg['From'] = from_addr msg['To'] = to_address msg.set_content( diff --git a/docker/routlin-dash/app/config_utils.py b/docker/routlin-dash/app/config_utils.py index 5ea507f..ad2b218 100644 --- a/docker/routlin-dash/app/config_utils.py +++ b/docker/routlin-dash/app/config_utils.py @@ -12,8 +12,10 @@ DASHBOARD_LAST_RUN = f'{CONFIGS_DIR}/.dashboard-last-run' DASHBOARD_LOCK = f'{CONFIGS_DIR}/.dashboard-lock' DASHBOARD_PENDING = f'{CONFIGS_DIR}/.dashboard-pending' STATUS_FILE = f'{CONFIGS_DIR}/.status' -DASHB_TIMER_NAME = 'routlin-dashboard-queue' -PRODUCT_DISPLAY_NAME = os.environ.get('PRODUCT_DISPLAY_NAME', 'Routlin Dashboard') +PRODUCT_NAME = os.environ.get('PRODUCT_NAME', 'routlin') +DASHB_TIMER_NAME = f'{PRODUCT_NAME}-dashboard-queue' +DDNS_TIMER_NAME = f'{PRODUCT_NAME}-ddns-update' +WEB_APP_DISPLAY_NAME = os.environ.get('WEB_APP_DISPLAY_NAME', f'{PRODUCT_NAME.capitalize()} Dashboard') DASHB_INTERVAL_SECS = 60 QUEUE_MAX_LINES = 50 diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 28b3fa3..12a1c47 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -5,7 +5,7 @@ import sanitize import validation as validate from datetime import datetime, timezone from zoneinfo import ZoneInfo, ZoneInfoNotFoundError -from config_utils import core_hash, get_pending_entries, get_dashboard_pending, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, PRODUCT_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR +from config_utils import core_hash, get_pending_entries, get_dashboard_pending, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR, DDNS_TIMER_NAME bp = Blueprint('view_page', __name__) @@ -540,7 +540,7 @@ def _public_ip_info(ddns_cfg): next_interval = '-' def _last_obtained(mtime): - return f'Last obtained: {_relative_time(mtime)}' if mtime else '' + return f'Obtained: {_relative_time(mtime)}' if mtime else '' # Path 1: timer healthy and within interval -> use cached IP if interval_secs and enabled_p: @@ -565,6 +565,22 @@ def _public_ip_info(ddns_cfg): # Path 3: offline return 'DDNS Offline', domains_sub, next_interval, '' +def _ddns_last_checked(): + """Return 'Last checked: X ago' based on when the DDNS timer last fired, or ''.""" + try: + out = _run(f'systemctl show {DDNS_TIMER_NAME}.timer --property=LastTriggerUSec --timestamp=utc') + val = out.split('=', 1)[1].strip() if '=' in out else '' + if not val or val == '0' or val == 'n/a': + return '' + parts = val.split() # ['Mon', '2026-05-25', '04:28:00', 'UTC'] + if len(parts) >= 3: + dt = datetime.strptime(f'{parts[1]} {parts[2]}', '%Y-%m-%d %H:%M:%S') + mtime = dt.replace(tzinfo=timezone.utc).timestamp() + return f'Last checked: {_relative_time(mtime)}' + except Exception: + pass + return '' + def _vpn_info(): for vlan in _load_core().get('vlans', []): if 'vpn_information' in vlan: @@ -711,10 +727,11 @@ def collect_tokens(): tokens['VPN_GATEWAY'] = '' ip_str, sub_str, next_interval, last_obtained = _public_ip_info(ddns) - tokens['STAT_PUBLIC_IP'] = ip_str - tokens['STAT_DDNS_HOSTNAME'] = sub_str - tokens['STAT_DDNS_NEXT_INTERVAL'] = next_interval + tokens['STAT_PUBLIC_IP'] = ip_str + tokens['STAT_DDNS_HOSTNAME'] = sub_str + tokens['STAT_DDNS_NEXT_INTERVAL'] = next_interval tokens['STAT_PUBLIC_IP_LAST_OBTAINED'] = last_obtained + tokens['STAT_PUBLIC_IP_LAST_CHECKED'] = _ddns_last_checked() tokens['DDNS_LOG_TAIL'], tokens['DDNS_LOG_SUMMARY'] = _ddns_log_tail() tokens['STAT_UPTIME'] = _run('uptime -p') or '-' tokens['STAT_NFTABLES_STATUS'] = 'Active' if _run('nft list tables 2>/dev/null').strip() else 'Inactive' @@ -1451,9 +1468,9 @@ def _load_datasource(spec): def render_layout(view_id, content_html, tokens): css = _load_css() level = _client_level() - titlebar_html = f'
' + titlebar_html = f'' navbar_html = _render_navbar(view_id, level, tokens) - footer_html = f'' + footer_html = f'' page_hash = core_hash() lan_iface = e(tokens.get('GENERAL_LAN_INTERFACE', '')) @@ -1535,8 +1552,10 @@ def render_layout(view_id, content_html, tokens): fix_items = ''.join(f'{e(c)}