from __future__ import annotations from urllib.parse import urlencode from ..ddns_models import DdnsSettings from ..ddns_security import admin_csrf_token from ..ddns_service import DdnsService from .html_utils import SafeHtml, attr, escape, trusted from .templates import _page, _top_nav from .templates_admin_tables import ( _account_row, _admin_table_pagination, _admin_table_toolbar, _domain_claim_row, _event_row, ) def _admin_secret_panel(title: str, rows: list[SafeHtml]) -> SafeHtml: return trusted(f"""

Admin action

{escape(title)}

Copy these values now. Secret values are only shown once.

{"".join(rows)}
""") def _render_admin_page( service: DdnsService, settings: DdnsSettings, accounts: list[dict[str, str | int | None]], domain_challenges: list[dict[str, str | None]], 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, notice_html: SafeHtml | None, ) -> str: csrf = attr(admin_csrf_token(settings)) admin_path = settings.admin_path safe_admin_path = attr(admin_path) safe_records_path = attr(f"{admin_path}/records") safe_events_path = attr(f"{admin_path}/events") rows = "\n".join(_account_row(account, csrf=csrf, admin_path=admin_path) for account in accounts) or """ No hostnames yet. """ domain_rows = "\n".join(_domain_claim_row(challenge) for challenge in domain_challenges) or """ No domain claims yet. """ event_rows = "\n".join(_event_row(event) for event in update_events) or """ No update events yet. """ accounts_count = escape(table_counts.get("accounts", len(accounts))) domain_count = escape(table_counts.get("domain_challenges", len(domain_challenges))) 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, "domain_claims_offset", table_offsets.get("domain_challenges", 0), table_limit, table_counts.get("domain_challenges", 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"""
{_top_nav("Operator", trusted(f'RecordsEvents'))}

KarlDNS

Operator console

Review hostnames, domain checks, updates, cleanup, and secret changes.

{notice_html or ""}

Service health

Service status

{cleanup_panel}
{metrics_html}

Hostnames

Hostnames

Showing {escape(len(accounts))} of {accounts_count}.

{_admin_table_toolbar("hostnames", "Search hostnames")}
{rows}
DomainUserIPv4IPv6UpdatedLinksActions
{_admin_table_pagination()} {account_pager}

Domain claims

Domain verification

Showing {escape(len(domain_challenges))} of {domain_count}.

{_admin_table_toolbar("domain-claims", "Search domain claims")}
{domain_rows}
DomainOwner dashboardStatusCreatedVerified
{_admin_table_pagination()} {domain_pager}

Updates

Update events

Showing {escape(len(update_events))} of {event_count}. Repeated checks with no IP change are grouped.

{_admin_table_toolbar("update-events", "Search update events")}
{event_rows}
TimeDomainStatusIPv4IPv6CountDetail
{_admin_table_pagination()} {event_pager}
"""), extra_styles=trusted(''), extra_scripts=trusted(''), ) def _server_page_links(admin_path: str, offset_name: str, offset: int, limit: int, total: int) -> SafeHtml: if total <= limit: return trusted("") previous_offset = max(0, offset - limit) next_offset = offset + limit previous_disabled = " disabled" if offset <= 0 else "" next_disabled = " disabled" if next_offset >= total else "" previous_href = attr(f"{admin_path}?{urlencode({offset_name: previous_offset})}") next_href = attr(f"{admin_path}?{urlencode({offset_name: next_offset})}") start = min(offset + 1, total) end = min(offset + limit, total) return trusted(f""" """) 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'

Last error: {escape(last_error)}

') 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"""
{escape(label)} {escape(value)}
""" for label, value in cards ) return trusted(f"""

Cleanup

Last cleanup removed {escape(domains_removed)} domain claims and {escape(accounts_removed)} unused hostnames.

{error_html}
{card_html}
""") 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"""
{escape(label)} {escape(value)}
""" for label, value in items ) return trusted(f"""

Metrics

{cards}
""") def _metric_int(value: float) -> str: return str(int(value))