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
def _read_cached_ip():
"""Return (ip, mtime) from the most recent .ddns-last-ip-* file, or ('', None)."""
try:
best_ip, best_mtime = '', 0
best_ip, best_mtime = '', 0.0
for fname in os.listdir(CONFIGS_DIR):
if fname.startswith('.ddns-last-ip-'):
path = f'{CONFIGS_DIR}/{fname}'
@ -523,12 +524,12 @@ def _read_cached_ip():
ip = open(path).read().strip()
if ip:
best_ip, best_mtime = ip, mtime
return best_ip
return best_ip, (best_mtime if best_ip else None)
except Exception:
return ''
return '', None
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'
enabled_p = [p for p in ddns_cfg.get('providers', []) if p.get('enabled', True)]
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', ''))
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
if interval_secs and enabled_p:
status = _run(f'python3 {script} --status 2>/dev/null')
@ -548,17 +552,18 @@ def _public_ip_info(ddns_cfg):
if remaining is not None:
next_interval = _fmt_seconds(remaining)
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:
return ip, domains_sub, next_interval
return ip, domains_sub, next_interval, _last_obtained(mtime)
# Path 2: live fetch
ip = _run(f'python3 {script} --getip 2>/dev/null')
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
return 'DDNS Offline', domains_sub, next_interval
return 'DDNS Offline', domains_sub, next_interval, ''
def _vpn_info():
for vlan in _load_core().get('vlans', []):
@ -705,10 +710,11 @@ def collect_tokens():
except Exception:
tokens['VPN_GATEWAY'] = ''
ip_str, sub_str, next_interval = _public_ip_info(ddns)
tokens['STAT_PUBLIC_IP'] = ip_str
tokens['STAT_DDNS_HOSTNAME'] = sub_str
tokens['STAT_DDNS_NEXT_INTERVAL'] = next_interval
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_LAST_OBTAINED'] = last_obtained
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'
@ -1490,9 +1496,7 @@ def render_layout(view_id, content_html, tokens):
sev = item.get('severity', 'error')
text = e(item.get('detail', item.get('name', '')))
tip = item.get('suggestion', '')
if tip:
text += f' <span style="opacity:0.75">{e(tip)}</span>'
grouped.setdefault(sev, []).append(text)
grouped.setdefault(sev, []).append((text, tip))
for item in st.get('services', []):
if item.get('status') == 'problem':
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)}.")
tip = f"Run `sudo python3 core.py --apply` to {' and '.join(reversed(fix_parts))} it."
sev = item.get('severity', 'error')
text = e(detail)
if tip:
text += f' <span style="opacity:0.75">{e(tip)}</span>'
grouped.setdefault(sev, []).append(text)
grouped.setdefault(sev, []).append((e(detail), tip))
for sev, items in grouped.items():
if not items:
continue
cls = 'info-bar-danger' if sev == 'error' else 'info-bar-warning'
if len(items) == 1:
content = items[0]
else:
content = '<ul style="margin:0;padding-left:1.25em">' + ''.join(f'<li>{t}</li>' for t in items) + '</ul>'
seen_cmds, fix_cmds = set(), []
for _, tip in items:
if tip:
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'
except Exception:
pass