Archived
Simplify admin operator UI
This commit is contained in:
1 parent
0fc220b90b
commit
30aef1fee8
7 files changed
+393
-105
No files matched your search
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from ..security import admin_csrf_token
|
||||
@@ -57,43 +58,39 @@ def _render_admin_page(
|
||||
account_pager = _server_page_links(admin_path, "accounts_offset", table_offsets.get("accounts", 0), table_limit, table_counts.get("accounts", 0))
|
||||
event_pager = _server_page_links(admin_path, "update_events_offset", table_offsets.get("update_events", 0), table_limit, table_counts.get("update_events", 0))
|
||||
cleanup_panel = _cleanup_panel(settings, cleanup_runs, scheduled_tasks, safe_admin_path, csrf)
|
||||
totals_html = _totals_strip(table_counts, service_metrics)
|
||||
return _page(
|
||||
"KarlDNS Admin",
|
||||
trusted(f"""
|
||||
<main id="main">
|
||||
{_top_nav("Operator")}
|
||||
<section class="section admin-heading">
|
||||
<section class="admin-console">
|
||||
<div class="container">
|
||||
<p class="eyebrow">KarlDNS</p>
|
||||
<h1>Operator console</h1>
|
||||
<p class="lead">Review hostnames, updates, cleanup, and secret changes.</p>
|
||||
<div class="admin-page-title">
|
||||
<div>
|
||||
<p class="eyebrow">KarlDNS</p>
|
||||
<h1>Operator console</h1>
|
||||
</div>
|
||||
{totals_html}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{notice_html or ""}
|
||||
|
||||
<section class="section">
|
||||
<section class="admin-block">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row mb-4">
|
||||
<div>
|
||||
<p class="eyebrow">Service health</p>
|
||||
<h2>Service status</h2>
|
||||
</div>
|
||||
</div>
|
||||
{cleanup_panel}
|
||||
<div class="mt-4">
|
||||
{metrics_html}
|
||||
</div>
|
||||
{metrics_html}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<section class="admin-block">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div class="admin-section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Hostnames</p>
|
||||
<h2>Hostnames</h2>
|
||||
<p class="section-copy mb-0">Showing {escape(len(accounts))} of {accounts_count}.</p>
|
||||
<p class="section-copy mb-0">{escape(len(accounts))} of {accounts_count} shown.</p>
|
||||
</div>
|
||||
</div>
|
||||
{_admin_table_toolbar("hostnames", "Search hostnames")}
|
||||
@@ -110,13 +107,12 @@ def _render_admin_page(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<section class="admin-block">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div class="admin-section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Updates</p>
|
||||
<h2>Update events</h2>
|
||||
<p class="section-copy mb-0">Showing {escape(len(update_events))} of {event_count}. Repeated checks with no IP change are grouped.</p>
|
||||
<p class="section-copy mb-0">{escape(len(update_events))} of {event_count} shown. Unchanged checks are grouped.</p>
|
||||
</div>
|
||||
</div>
|
||||
{_admin_table_toolbar("update-events", "Search update events")}
|
||||
@@ -132,11 +128,37 @@ def _render_admin_page(
|
||||
</section>
|
||||
</main>
|
||||
"""),
|
||||
extra_styles=trusted('<link href="/admin.css?v=3" rel="stylesheet">'),
|
||||
extra_styles=trusted('<link href="/admin.css?v=4" rel="stylesheet">'),
|
||||
extra_scripts=trusted('<script src="/admin-tables.js?v=2" defer></script>'),
|
||||
)
|
||||
|
||||
|
||||
def _totals_strip(table_counts: dict[str, int], metrics: dict[str, float]) -> SafeHtml:
|
||||
items = [
|
||||
("Hostnames", table_counts.get("accounts", 0)),
|
||||
("Events", table_counts.get("update_events", 0)),
|
||||
("DNS failures", int(metrics.get("dns_failures_total", 0))),
|
||||
("Blocked", int(metrics.get("rate_limit_hits_total", 0))),
|
||||
]
|
||||
return trusted(
|
||||
'<div class="admin-total-strip" aria-label="Service totals">'
|
||||
+ "".join(
|
||||
f'<span><strong>{escape(value)}</strong>{escape(label)}</span>'
|
||||
for label, value in items
|
||||
)
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
|
||||
def _admin_notice(kind: str, message: str) -> SafeHtml:
|
||||
alert_class = "alert-success" if kind == "success" else "alert-info"
|
||||
return trusted(f"""
|
||||
<div class="container">
|
||||
<div class="alert {alert_class} admin-notice" role="status">{escape(message)}</div>
|
||||
</div>
|
||||
""")
|
||||
|
||||
|
||||
def _server_page_links(admin_path: str, offset_name: str, offset: int, limit: int, total: int) -> SafeHtml:
|
||||
if total <= limit:
|
||||
return trusted("")
|
||||
@@ -149,7 +171,7 @@ def _server_page_links(admin_path: str, offset_name: str, offset: int, limit: in
|
||||
start = min(offset + 1, total)
|
||||
end = min(offset + limit, total)
|
||||
return trusted(f"""
|
||||
<nav class="d-flex align-items-center justify-content-end gap-2 mt-3" aria-label="Server-side table pages">
|
||||
<nav class="d-flex align-items-center justify-content-end gap-2 mt-2" aria-label="Server-side table pages">
|
||||
<span class="small text-secondary">{escape(start)}-{escape(end)} of {escape(total)}</span>
|
||||
<a class="btn btn-sm btn-light rounded-pill{previous_disabled}" href="{previous_href}" aria-disabled="{str(offset <= 0).lower()}">Previous</a>
|
||||
<a class="btn btn-sm btn-light rounded-pill{next_disabled}" href="{next_href}" aria-disabled="{str(next_offset >= total).lower()}">Next</a>
|
||||
@@ -177,41 +199,27 @@ def _cleanup_panel(
|
||||
if last_error
|
||||
else trusted("")
|
||||
)
|
||||
cards = [
|
||||
("Interval", f"*/{interval}s"),
|
||||
("Last run", str(last_run or "Never")),
|
||||
("Next run", str(next_run or "Pending")),
|
||||
("Status", str(status)),
|
||||
]
|
||||
card_html = "\n".join(
|
||||
f"""
|
||||
<div class="col-12 col-md-6 col-xl-3">
|
||||
<div class="metric-card h-100">
|
||||
<span>{escape(label)}</span>
|
||||
<strong>{escape(value)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
for label, value in cards
|
||||
)
|
||||
last_run_title = attr(str(last_run or "Never"))
|
||||
next_run_title = attr(str(next_run or "Pending"))
|
||||
last_run_display = escape(_format_timestamp(last_run, "Never"))
|
||||
next_run_display = escape(_format_timestamp(next_run, "Pending"))
|
||||
return trusted(f"""
|
||||
<div class="card flat-card">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 flex-column flex-lg-row">
|
||||
<div>
|
||||
<h3 class="h5 mb-2">Cleanup</h3>
|
||||
<p class="section-copy mb-1">Last cleanup removed {escape(accounts_removed)} unused hostnames.</p>
|
||||
{error_html}
|
||||
</div>
|
||||
<form method="post" action="{safe_admin_path}/cleanup" aria-label="Run cleanup now">
|
||||
<input type="hidden" name="csrf" value="{csrf}">
|
||||
<button type="submit" class="btn btn-primary rounded-pill">Run now</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="row g-3 mt-3">
|
||||
{card_html}
|
||||
</div>
|
||||
<div class="admin-cleanup-bar">
|
||||
<div class="admin-cleanup-title">
|
||||
<h2>Cleanup</h2>
|
||||
<span>Every {escape(_format_interval(interval))}</span>
|
||||
</div>
|
||||
<dl class="admin-cleanup-times">
|
||||
<div><dt>Last run</dt><dd title="{last_run_title}">{last_run_display}</dd></div>
|
||||
<div><dt>Next run</dt><dd title="{next_run_title}">{next_run_display}</dd></div>
|
||||
<div><dt>Status</dt><dd>{escape(str(status))}</dd></div>
|
||||
<div><dt>Removed</dt><dd>{escape(accounts_removed)}</dd></div>
|
||||
</dl>
|
||||
{error_html}
|
||||
<form method="post" action="{safe_admin_path}/cleanup" aria-label="Run cleanup now">
|
||||
<input type="hidden" name="csrf" value="{csrf}">
|
||||
<button type="submit" class="btn btn-primary rounded-pill">Run now</button>
|
||||
</form>
|
||||
</div>
|
||||
""")
|
||||
|
||||
@@ -231,22 +239,41 @@ def _metrics_panel(metrics: dict[str, float]) -> SafeHtml:
|
||||
]
|
||||
cards = "\n".join(
|
||||
f"""
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="metric-card h-100">
|
||||
<span>{escape(label)}</span>
|
||||
<strong>{escape(value)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<span><strong>{escape(value)}</strong>{escape(label)}</span>
|
||||
"""
|
||||
for label, value in items
|
||||
)
|
||||
return trusted(f"""
|
||||
<div class="mt-4">
|
||||
<p class="eyebrow">Metrics</p>
|
||||
<div class="row g-3 mt-1">{cards}</div>
|
||||
<div class="admin-metric-strip" aria-label="Service metrics">
|
||||
{cards}
|
||||
</div>
|
||||
""")
|
||||
|
||||
|
||||
def _metric_int(value: float) -> str:
|
||||
return str(int(value))
|
||||
|
||||
|
||||
def _format_interval(seconds: object) -> str:
|
||||
try:
|
||||
value = int(seconds)
|
||||
except (TypeError, ValueError):
|
||||
return str(seconds)
|
||||
if value % 3600 == 0:
|
||||
hours = value // 3600
|
||||
return f"{hours}h"
|
||||
if value % 60 == 0:
|
||||
minutes = value // 60
|
||||
return f"{minutes}m"
|
||||
return f"{value}s"
|
||||
|
||||
|
||||
def _format_timestamp(value: object, fallback: str = "-") -> str:
|
||||
if not value:
|
||||
return fallback
|
||||
raw = str(value)
|
||||
try:
|
||||
parsed = datetime.fromisoformat(raw)
|
||||
except ValueError:
|
||||
return raw
|
||||
return parsed.strftime("%Y-%m-%d %H:%M")
|
||||
Reference in new issue
Block a user