Development

This commit is contained in:
Matthew Grotke 2026-05-24 01:23:46 -04:00
parent 80390334fb
commit e98eb85c5a
2 changed files with 39 additions and 24 deletions

View file

@ -513,8 +513,9 @@ def _parse_time_remaining(text):
return None return None
def _read_cached_ip(): def _read_cached_ip():
"""Return (ip, mtime) from the most recent .ddns-last-ip-* file, or ('', None)."""
try: try:
best_ip, best_mtime = '', 0 best_ip, best_mtime = '', 0.0
for fname in os.listdir(CONFIGS_DIR): for fname in os.listdir(CONFIGS_DIR):
if fname.startswith('.ddns-last-ip-'): if fname.startswith('.ddns-last-ip-'):
path = f'{CONFIGS_DIR}/{fname}' path = f'{CONFIGS_DIR}/{fname}'
@ -523,12 +524,12 @@ def _read_cached_ip():
ip = open(path).read().strip() ip = open(path).read().strip()
if ip: if ip:
best_ip, best_mtime = ip, mtime best_ip, best_mtime = ip, mtime
return best_ip return best_ip, (best_mtime if best_ip else None)
except Exception: except Exception:
return '' return '', None
def _public_ip_info(ddns_cfg): def _public_ip_info(ddns_cfg):
"""Return (ip_str, domains_sub, next_interval_str) for stat cards.""" """Return (ip_str, domains_sub, next_interval_str, last_obtained_str) for stat cards."""
script = f'{CONFIGS_DIR}/ddns.py' script = f'{CONFIGS_DIR}/ddns.py'
enabled_p = [p for p in ddns_cfg.get('providers', []) if p.get('enabled', True)] enabled_p = [p for p in ddns_cfg.get('providers', []) if p.get('enabled', True)]
all_hosts = [] all_hosts = []
@ -538,6 +539,9 @@ def _public_ip_info(ddns_cfg):
interval_secs = _parse_interval_to_seconds(ddns_cfg.get('general', {}).get('timer_interval', '')) interval_secs = _parse_interval_to_seconds(ddns_cfg.get('general', {}).get('timer_interval', ''))
next_interval = '-' next_interval = '-'
def _last_obtained(mtime):
return f'Last obtained: {_relative_time(mtime)}' if mtime else ''
# Path 1: timer healthy and within interval -> use cached IP # Path 1: timer healthy and within interval -> use cached IP
if interval_secs and enabled_p: if interval_secs and enabled_p:
status = _run(f'python3 {script} --status 2>/dev/null') status = _run(f'python3 {script} --status 2>/dev/null')
@ -548,17 +552,18 @@ def _public_ip_info(ddns_cfg):
if remaining is not None: if remaining is not None:
next_interval = _fmt_seconds(remaining) next_interval = _fmt_seconds(remaining)
if is_enabled and is_active and remaining is not None and remaining < interval_secs: if is_enabled and is_active and remaining is not None and remaining < interval_secs:
ip = _read_cached_ip() ip, mtime = _read_cached_ip()
if ip: if ip:
return ip, domains_sub, next_interval return ip, domains_sub, next_interval, _last_obtained(mtime)
# Path 2: live fetch # Path 2: live fetch
ip = _run(f'python3 {script} --getip 2>/dev/null') ip = _run(f'python3 {script} --getip 2>/dev/null')
if ip and re.match(r'^\d{1,3}(\.\d{1,3}){3}$', ip): if ip and re.match(r'^\d{1,3}(\.\d{1,3}){3}$', ip):
return ip, domains_sub, next_interval _, mtime = _read_cached_ip()
return ip, domains_sub, next_interval, _last_obtained(mtime)
# Path 3: offline # Path 3: offline
return 'DDNS Offline', domains_sub, next_interval return 'DDNS Offline', domains_sub, next_interval, ''
def _vpn_info(): def _vpn_info():
for vlan in _load_core().get('vlans', []): for vlan in _load_core().get('vlans', []):
@ -705,10 +710,11 @@ def collect_tokens():
except Exception: except Exception:
tokens['VPN_GATEWAY'] = '' tokens['VPN_GATEWAY'] = ''
ip_str, sub_str, next_interval = _public_ip_info(ddns) ip_str, sub_str, next_interval, last_obtained = _public_ip_info(ddns)
tokens['STAT_PUBLIC_IP'] = ip_str tokens['STAT_PUBLIC_IP'] = ip_str
tokens['STAT_DDNS_HOSTNAME'] = sub_str tokens['STAT_DDNS_HOSTNAME'] = sub_str
tokens['STAT_DDNS_NEXT_INTERVAL'] = next_interval tokens['STAT_DDNS_NEXT_INTERVAL'] = next_interval
tokens['STAT_PUBLIC_IP_LAST_OBTAINED'] = last_obtained
tokens['DDNS_LOG_TAIL'], tokens['DDNS_LOG_SUMMARY'] = _ddns_log_tail() tokens['DDNS_LOG_TAIL'], tokens['DDNS_LOG_SUMMARY'] = _ddns_log_tail()
tokens['STAT_UPTIME'] = _run('uptime -p') or '-' tokens['STAT_UPTIME'] = _run('uptime -p') or '-'
tokens['STAT_NFTABLES_STATUS'] = 'Active' if _run('nft list tables 2>/dev/null').strip() else 'Inactive' tokens['STAT_NFTABLES_STATUS'] = 'Active' if _run('nft list tables 2>/dev/null').strip() else 'Inactive'
@ -1490,9 +1496,7 @@ def render_layout(view_id, content_html, tokens):
sev = item.get('severity', 'error') sev = item.get('severity', 'error')
text = e(item.get('detail', item.get('name', ''))) text = e(item.get('detail', item.get('name', '')))
tip = item.get('suggestion', '') tip = item.get('suggestion', '')
if tip: grouped.setdefault(sev, []).append((text, tip))
text += f' <span style="opacity:0.75">{e(tip)}</span>'
grouped.setdefault(sev, []).append(text)
for item in st.get('services', []): for item in st.get('services', []):
if item.get('status') == 'problem': if item.get('status') == 'problem':
name = item.get('name', '') name = item.get('name', '')
@ -1510,18 +1514,29 @@ def render_layout(view_id, content_html, tokens):
f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.") f"{' and '.join(exp_parts)} but is {' and '.join(act_parts)}.")
tip = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it." tip = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it."
sev = item.get('severity', 'error') sev = item.get('severity', 'error')
text = e(detail) grouped.setdefault(sev, []).append((e(detail), tip))
if tip:
text += f' <span style="opacity:0.75">{e(tip)}</span>'
grouped.setdefault(sev, []).append(text)
for sev, items in grouped.items(): for sev, items in grouped.items():
if not items: if not items:
continue continue
cls = 'info-bar-danger' if sev == 'error' else 'info-bar-warning' cls = 'info-bar-danger' if sev == 'error' else 'info-bar-warning'
if len(items) == 1: seen_cmds, fix_cmds = set(), []
content = items[0] for _, tip in items:
else: if tip:
content = '<ul style="margin:0;padding-left:1.25em">' + ''.join(f'<li>{t}</li>' for t in items) + '</ul>' m = re.search(r'`([^`]+)`', tip)
cmd = m.group(1) if m else tip
if cmd not in seen_cmds:
seen_cmds.add(cmd)
fix_cmds.append(cmd)
problems_list = ('<ul style="margin:0.25em 0;padding-left:1.25em">'
+ ''.join(f'<li>{d}</li>' for d, _ in items)
+ '</ul>')
fix_html = ''
if fix_cmds:
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)
problem_bars += f'<div class="info-bar {cls}">{content}</div>\n' problem_bars += f'<div class="info-bar {cls}">{content}</div>\n'
except Exception: except Exception:
pass pass

View file

@ -313,7 +313,7 @@
"type": "stat_card", "type": "stat_card",
"label": "Current Public IP", "label": "Current Public IP",
"value": "%STAT_PUBLIC_IP%", "value": "%STAT_PUBLIC_IP%",
"sub": "" "sub": "%STAT_PUBLIC_IP_LAST_OBTAINED%"
}, },
{ {
"type": "stat_card", "type": "stat_card",