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

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