From 278995958a51f4200ae93297fc0b4d43bdc52653 Mon Sep 17 00:00:00 2001 From: Matthew Grotke Date: Sun, 24 May 2026 01:46:48 -0400 Subject: [PATCH] Development --- docker/routlin-dash/app/action_apply_vpn.py | 4 +- .../routlin-dash/app/action_create_account.py | 4 +- docker/routlin-dash/app/config_utils.py | 6 ++- docker/routlin-dash/app/view_page.py | 39 ++++++++++++++----- docker/routlin-dash/data/page_content.json | 2 +- docker/routlin-dash/docker-compose.yml | 2 +- 6 files changed, 39 insertions(+), 18 deletions(-) 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'
{PRODUCT_DISPLAY_NAME}
' + titlebar_html = f'
{WEB_APP_DISPLAY_NAME}
' 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)}
  • ' for c in fix_cmds) fix_html = ('
    To fix:
    ' f'') - content = ('Health check — problems found:' - + problems_list + fix_html) + content = ('
    ' + '
    Health check — problems found:
    ' + + problems_list + fix_html + + '
    ') problem_bars += f'
    {content}
    \n' except Exception: pass @@ -1544,7 +1563,7 @@ def render_layout(view_id, content_html, tokens): return (f'\n\n\n' f' \n' f' \n' - f' {PRODUCT_DISPLAY_NAME}\n' + f' {WEB_APP_DISPLAY_NAME}\n' f' \n' f'\n\n' f'{titlebar_html}\n' diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index 99816cd..1370ff7 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -319,7 +319,7 @@ "type": "stat_card", "label": "Check Interval", "value": "%DDNS_TIMER_INTERVAL%", - "sub": "next in %STAT_DDNS_NEXT_INTERVAL%" + "sub": "%STAT_PUBLIC_IP_LAST_CHECKED%" }, { "type": "stat_card", diff --git a/docker/routlin-dash/docker-compose.yml b/docker/routlin-dash/docker-compose.yml index c2bfa61..689c55b 100644 --- a/docker/routlin-dash/docker-compose.yml +++ b/docker/routlin-dash/docker-compose.yml @@ -13,7 +13,7 @@ services: - /sys/devices:/sys/devices:ro environment: - PYTHONPATH=/routlin_location - - PRODUCT_DISPLAY_NAME=Routlin Dashboard + - WEB_APP_DISPLAY_NAME=Routlin Dashboard - INITIAL_MANAGER_EMAIL=mgrotke@gmail.com - SECRET_KEY=ey8hSQCCYE5kQXV8nOg1CB44LSd3AoUet2ZBc3aZlFrwBbazE7aHcxXWyuT97eAObet5jmOL0CjMg0rB1hE4d2SBVYHPfl8De55EiFv307r1QP3Mf5XgOSSCxD3TuD - SMTP_HOST=smtp.gmail.com