Archived
Polish KarlDNS production UI and docs
This commit is contained in:
1 parent
ca92dc211a
commit
5beee0e5c6
61 files changed
+1566
-523
No files matched your search
@@ -11,10 +11,8 @@ from .templates_admin_tables import (
|
||||
_account_row,
|
||||
_admin_table_pagination,
|
||||
_admin_table_toolbar,
|
||||
_cleanup_row,
|
||||
_domain_claim_row,
|
||||
_event_row,
|
||||
_scheduled_task_row,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,7 +22,7 @@ def _admin_secret_panel(title: str, rows: list[SafeHtml]) -> SafeHtml:
|
||||
<div class="container">
|
||||
<p class="eyebrow">Admin action</p>
|
||||
<h2>{escape(title)}</h2>
|
||||
<p class="section-copy">Copy this now. Secret values are only shown in this response.</p>
|
||||
<p class="section-copy">Copy these values now. Secret values are only shown once.</p>
|
||||
<div class="d-grid gap-2">
|
||||
{"".join(rows)}
|
||||
</div>
|
||||
@@ -40,6 +38,7 @@ def _render_admin_page(
|
||||
cleanup_runs: list[dict[str, str | int]],
|
||||
scheduled_tasks: list[dict[str, str | int | None]],
|
||||
update_events: list[dict[str, str | int | None]],
|
||||
service_metrics: dict[str, float],
|
||||
table_counts: dict[str, int],
|
||||
table_offsets: dict[str, int],
|
||||
table_limit: int,
|
||||
@@ -56,19 +55,13 @@ def _render_admin_page(
|
||||
domain_rows = "\n".join(_domain_claim_row(challenge) for challenge in domain_challenges) or """
|
||||
<tr class="empty-row"><td colspan="5" class="empty">No domain claims yet.</td></tr>
|
||||
"""
|
||||
cleanup_rows = "\n".join(_cleanup_row(run) for run in cleanup_runs) or """
|
||||
<tr class="empty-row"><td colspan="3" class="empty">No cleanup runs yet.</td></tr>
|
||||
"""
|
||||
scheduled_task_rows = "\n".join(_scheduled_task_row(task) for task in scheduled_tasks) or """
|
||||
<tr class="empty-row"><td colspan="6" class="empty">No scheduled tasks yet.</td></tr>
|
||||
"""
|
||||
event_rows = "\n".join(_event_row(event) for event in update_events) or """
|
||||
<tr class="empty-row"><td colspan="7" class="empty">No update events yet.</td></tr>
|
||||
"""
|
||||
accounts_count = escape(table_counts.get("accounts", len(accounts)))
|
||||
domain_count = escape(table_counts.get("domain_challenges", len(domain_challenges)))
|
||||
cleanup_count = escape(table_counts.get("cleanup_runs", len(cleanup_runs)))
|
||||
event_count = escape(table_counts.get("update_events", len(update_events)))
|
||||
metrics_html = _metrics_panel(service_metrics)
|
||||
account_pager = _server_page_links(admin_path, "accounts_offset", table_offsets.get("accounts", 0), table_limit, table_counts.get("accounts", 0))
|
||||
domain_pager = _server_page_links(
|
||||
admin_path,
|
||||
@@ -77,18 +70,18 @@ def _render_admin_page(
|
||||
table_limit,
|
||||
table_counts.get("domain_challenges", 0),
|
||||
)
|
||||
cleanup_pager = _server_page_links(admin_path, "cleanup_runs_offset", table_offsets.get("cleanup_runs", 0), table_limit, table_counts.get("cleanup_runs", 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)
|
||||
return _page(
|
||||
"KarlDNS Admin",
|
||||
trusted(f"""
|
||||
<main id="main">
|
||||
{_top_nav("Operator console", trusted(f'<a class="nav-link px-0" href="{safe_records_path}">Records</a><a class="nav-link px-0" href="{safe_events_path}">Events</a>'))}
|
||||
{_top_nav("Operator", trusted(f'<a class="nav-link px-0" href="{safe_records_path}">Records</a><a class="nav-link px-0" href="{safe_events_path}">Events</a>'))}
|
||||
<section class="section admin-heading">
|
||||
<div class="container">
|
||||
<p class="eyebrow">KarlDNS aktiv</p>
|
||||
<p class="eyebrow">KarlDNS</p>
|
||||
<h1>Operator console</h1>
|
||||
<p class="lead">Inspect hostnames, verify domain claims, rotate links, run cleanup, and review router update events.</p>
|
||||
<p class="lead">Review hostnames, domain checks, updates, cleanup, and secret changes.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -96,17 +89,15 @@ def _render_admin_page(
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<p class="eyebrow">Service health</p>
|
||||
<h2>Scheduled cleanup</h2>
|
||||
<p class="section-copy">Cleanup runs every {escape(settings.cleanup_interval_seconds)} seconds. It removes unverified domain claims after {escape(settings.cleanup_challenge_hours)} hours and never-used generated hostnames after {escape(settings.cleanup_unused_account_hours)} hours.</p>
|
||||
<p class="section-copy">The scheduler state is stored in SQLite. Multiple app workers coordinate with a database lease, so only one worker runs a due cleanup task.</p>
|
||||
<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>
|
||||
<form method="post" action="{safe_admin_path}/cleanup" class="col-lg-5 card card-body gap-3" aria-label="Run cleanup now">
|
||||
<input type="hidden" name="csrf" value="{csrf}">
|
||||
<button type="submit" class="btn btn-primary rounded-pill">Run cleanup now</button>
|
||||
</form>
|
||||
{cleanup_panel}
|
||||
<div class="mt-4">
|
||||
{metrics_html}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -116,15 +107,15 @@ def _render_admin_page(
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div>
|
||||
<p class="eyebrow">Hostnames</p>
|
||||
<h2>Active credentials</h2>
|
||||
<p class="section-copy mb-0">Showing up to {escape(len(accounts))} of {accounts_count} hostnames.</p>
|
||||
<h2>Hostnames</h2>
|
||||
<p class="section-copy mb-0">Showing {escape(len(accounts))} of {accounts_count}.</p>
|
||||
</div>
|
||||
</div>
|
||||
{_admin_table_toolbar("hostnames", "Search hostnames")}
|
||||
<div class="table-responsive border rounded admin-table-scroll">
|
||||
<table class="table mb-0">
|
||||
<thead>
|
||||
<tr><th>Domain</th><th>User</th><th>IPv4</th><th>IPv6</th><th>Updated</th><th>Links</th><th></th></tr>
|
||||
<tr><th scope="col">Domain</th><th scope="col">User</th><th scope="col">IPv4</th><th scope="col">IPv6</th><th scope="col">Updated</th><th scope="col">Links</th><th scope="col">Actions</th></tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
@@ -139,14 +130,14 @@ def _render_admin_page(
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div>
|
||||
<p class="eyebrow">Domain claims</p>
|
||||
<h2>Verification lifecycle</h2>
|
||||
<p class="section-copy mb-0">Showing up to {escape(len(domain_challenges))} of {domain_count} claims.</p>
|
||||
<h2>Domain verification</h2>
|
||||
<p class="section-copy mb-0">Showing {escape(len(domain_challenges))} of {domain_count}.</p>
|
||||
</div>
|
||||
</div>
|
||||
{_admin_table_toolbar("domain-claims", "Search domain claims")}
|
||||
<div class="table-responsive border rounded admin-table-scroll">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Domain</th><th>Owner dashboard</th><th>Status</th><th>Created</th><th>Verified</th></tr></thead>
|
||||
<thead><tr><th scope="col">Domain</th><th scope="col">Owner dashboard</th><th scope="col">Status</th><th scope="col">Created</th><th scope="col">Verified</th></tr></thead>
|
||||
<tbody>{domain_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -155,52 +146,19 @@ def _render_admin_page(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div>
|
||||
<p class="eyebrow">Cleanup</p>
|
||||
<h2>Scheduler state</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive border rounded admin-table-scroll mb-4">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Task</th><th>Status</th><th>Next run</th><th>Lease expires</th><th>Last finished</th><th>Last error</th></tr></thead>
|
||||
<tbody>{scheduled_task_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div>
|
||||
<p class="eyebrow">Cleanup</p>
|
||||
<h2>Recent runs</h2>
|
||||
<p class="section-copy mb-0">Showing up to {escape(len(cleanup_runs))} of {cleanup_count} runs.</p>
|
||||
</div>
|
||||
</div>
|
||||
{_admin_table_toolbar("cleanup-runs", "Search cleanup runs")}
|
||||
<div class="table-responsive border rounded admin-table-scroll">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Time</th><th>Domain claims removed</th><th>Unused hostnames removed</th></tr></thead>
|
||||
<tbody>{cleanup_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{_admin_table_pagination()}
|
||||
{cleanup_pager}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
|
||||
<div>
|
||||
<p class="eyebrow">Updates</p>
|
||||
<h2>Recent events</h2>
|
||||
<p class="section-copy mb-0">Showing up to {escape(len(update_events))} of {event_count} event rows. Repeated unchanged polls are aggregated.</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>
|
||||
</div>
|
||||
</div>
|
||||
{_admin_table_toolbar("update-events", "Search update events")}
|
||||
<div class="table-responsive border rounded admin-table-scroll">
|
||||
<table class="table mb-0">
|
||||
<thead><tr><th>Time</th><th>Domain</th><th>Status</th><th>IPv4</th><th>IPv6</th><th>Count</th><th>Detail</th></tr></thead>
|
||||
<thead><tr><th scope="col">Time</th><th scope="col">Domain</th><th scope="col">Status</th><th scope="col">IPv4</th><th scope="col">IPv6</th><th scope="col">Count</th><th scope="col">Detail</th></tr></thead>
|
||||
<tbody>{event_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -210,6 +168,8 @@ def _render_admin_page(
|
||||
</section>
|
||||
</main>
|
||||
"""),
|
||||
extra_styles=trusted('<link href="/admin.css?v=2" rel="stylesheet">'),
|
||||
extra_scripts=trusted('<script src="/admin-tables.js?v=2" defer></script>'),
|
||||
)
|
||||
|
||||
|
||||
@@ -227,7 +187,103 @@ def _server_page_links(admin_path: str, offset_name: str, offset: int, limit: in
|
||||
return trusted(f"""
|
||||
<nav class="d-flex align-items-center justify-content-end gap-2 mt-3" 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 server page</a>
|
||||
<a class="btn btn-sm btn-light rounded-pill{next_disabled}" href="{next_href}" aria-disabled="{str(next_offset >= total).lower()}">Next server page</a>
|
||||
<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>
|
||||
</nav>
|
||||
""")
|
||||
|
||||
|
||||
def _cleanup_panel(
|
||||
settings: DdnsSettings,
|
||||
cleanup_runs: list[dict[str, str | int]],
|
||||
scheduled_tasks: list[dict[str, str | int | None]],
|
||||
safe_admin_path: str,
|
||||
csrf: str,
|
||||
) -> SafeHtml:
|
||||
task = next((item for item in scheduled_tasks if item.get("name") == "cleanup"), scheduled_tasks[0] if scheduled_tasks else {})
|
||||
last_run = task.get("last_finished_at") or (cleanup_runs[0].get("created_at") if cleanup_runs else None)
|
||||
next_run = task.get("next_run_at")
|
||||
status = task.get("last_status") or "pending"
|
||||
last_error = task.get("last_error") or ""
|
||||
interval = task.get("interval_seconds") or settings.cleanup_interval_seconds
|
||||
last_cleanup = cleanup_runs[0] if cleanup_runs else {}
|
||||
domains_removed = last_cleanup.get("domain_challenges_deleted", 0)
|
||||
accounts_removed = last_cleanup.get("unused_accounts_deleted", 0)
|
||||
error_html = (
|
||||
trusted(f'<p class="small text-danger mb-0">Last error: {escape(last_error)}</p>')
|
||||
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
|
||||
)
|
||||
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(domains_removed)} domain claims and {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>
|
||||
</div>
|
||||
""")
|
||||
|
||||
|
||||
def _metrics_panel(metrics: dict[str, float]) -> SafeHtml:
|
||||
samples = metrics.get("update_latency_samples_total", 0)
|
||||
average_latency = metrics.get("update_latency_ms_total", 0) / samples if samples else 0
|
||||
items = [
|
||||
("Updates", _metric_int(metrics.get("update_requests_total", 0))),
|
||||
("Updated", _metric_int(metrics.get("update_success_total", 0))),
|
||||
("DNS failures", _metric_int(metrics.get("dns_failures_total", 0))),
|
||||
("DNS check failures", _metric_int(metrics.get("dns_reconcile_failures_total", 0))),
|
||||
("Blocked requests", _metric_int(metrics.get("rate_limit_hits_total", 0))),
|
||||
("Average update time", f"{average_latency:.1f} ms"),
|
||||
("Last update time", f"{metrics.get('last_update_latency_ms', 0):.1f} ms"),
|
||||
("Cleanup ok/errors", f"{_metric_int(metrics.get('cleanup_success_total', 0))}/{_metric_int(metrics.get('cleanup_errors_total', 0))}"),
|
||||
]
|
||||
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>
|
||||
"""
|
||||
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>
|
||||
""")
|
||||
|
||||
|
||||
def _metric_int(value: float) -> str:
|
||||
return str(int(value))
|
||||
Reference in new issue
Block a user