Development

This commit is contained in:
Matthew Grotke 2026-05-24 01:46:48 -04:00
parent e98eb85c5a
commit 278995958a
6 changed files with 39 additions and 18 deletions

View file

@ -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}',

View file

@ -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(

View file

@ -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

View file

@ -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'<div class="titlebar"><span class="titlebar-brand">{PRODUCT_DISPLAY_NAME}</span></div>'
titlebar_html = f'<div class="titlebar"><span class="titlebar-brand">{WEB_APP_DISPLAY_NAME}</span></div>'
navbar_html = _render_navbar(view_id, level, tokens)
footer_html = f'<footer class="footer">{PRODUCT_DISPLAY_NAME}</footer>'
footer_html = f'<footer class="footer">{WEB_APP_DISPLAY_NAME}</footer>'
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'<li><code>{e(c)}</code></li>' for c in fix_cmds)
fix_html = ('<div style="margin-top:0.5em">To fix:</div>'
f'<ul style="margin:0.25em 0;padding-left:1.25em">{fix_items}</ul>')
content = ('Health check &mdash; problems found:'
+ problems_list + fix_html)
content = ('<div style="width:100%">'
'<div style="font-weight:600;margin-bottom:0.25em">Health check &mdash; problems found:</div>'
+ problems_list + fix_html
+ '</div>')
problem_bars += f'<div class="info-bar {cls}">{content}</div>\n'
except Exception:
pass
@ -1544,7 +1563,7 @@ def render_layout(view_id, content_html, tokens):
return (f'<!DOCTYPE html>\n<html lang="en">\n<head>\n'
f' <meta charset="UTF-8"/>\n'
f' <meta name="viewport" content="width=device-width, initial-scale=1.0"/>\n'
f' <title>{PRODUCT_DISPLAY_NAME}</title>\n'
f' <title>{WEB_APP_DISPLAY_NAME}</title>\n'
f' <style>{css}</style>\n'
f'</head>\n<body>\n'
f'{titlebar_html}\n'

View file

@ -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",

View file

@ -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