from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime
from urllib.parse import urlencode
from ..security import admin_csrf_token
from ..settings import DdnsSettings
from ..store import DdnsStore
from .html_utils import SafeHtml, attr, escape, trusted
from .layout import PageOptions, _page, _top_nav
from .templates_admin_tables import (
_account_row,
_admin_table_pagination,
_admin_table_toolbar,
_event_row,
)
@dataclass(frozen=True)
class _CleanupSummary:
last_run: object
next_run: object
status: object
last_error: object
interval_seconds: object
accounts_removed: object
@dataclass(frozen=True)
class AdminPageContext:
accounts: Sequence[Mapping[str, object]]
cleanup_runs: Sequence[Mapping[str, object]]
scheduled_tasks: Sequence[Mapping[str, object]]
update_events: Sequence[Mapping[str, object]]
service_metrics: dict[str, float]
table_counts: dict[str, int]
table_offsets: dict[str, int]
table_limit: int
notice_html: SafeHtml | None
def _admin_page_context(
store: DdnsStore,
table_offsets: dict[str, int],
table_limit: int,
notice_html: SafeHtml | None,
) -> AdminPageContext:
return AdminPageContext(
accounts=store.list_accounts(limit=table_limit, offset=table_offsets.get("accounts", 0)),
cleanup_runs=store.list_cleanup_runs(limit=table_limit, offset=table_offsets.get("cleanup_runs", 0)),
scheduled_tasks=store.list_scheduled_tasks(),
update_events=store.list_update_events(table_limit, offset=table_offsets.get("update_events", 0)),
service_metrics=store.get_service_metrics(),
table_counts={
"accounts": store.count_accounts(),
"cleanup_runs": store.count_cleanup_runs(),
"update_events": store.count_update_events(),
},
table_offsets=table_offsets,
table_limit=table_limit,
notice_html=notice_html,
)
def _admin_secret_panel(title: str, rows: list[SafeHtml]) -> SafeHtml:
return trusted(f"""
Admin action Copy these values now. Secret values are only shown once. KarlDNS {escape(len(context.accounts))} of {accounts_count} shown.
{escape(len(context.update_events))} of {event_count} shown. Unchanged checks are grouped.
{escape(title)}
Operator console
Hostnames
{rows}
Hostname
Status
IPv4
IPv6
Updated
Links
Actions
Update events
{event_rows}
Time
Hostname
Status
IPv4
IPv6
Count
Detail
Last error: {escape(summary.last_error)}
') if summary.last_error else trusted("") ) last_run_title = attr(str(summary.last_run or "Never")) next_run_title = attr(str(summary.next_run or "Pending")) last_run_display = escape(_format_timestamp(summary.last_run, "Never")) next_run_display = escape(_format_timestamp(summary.next_run, "Pending")) return trusted(f""" """) def _cleanup_task(scheduled_tasks: Sequence[Mapping[str, object]]) -> Mapping[str, object]: return next( (item for item in scheduled_tasks if item.get("name") == "cleanup"), scheduled_tasks[0] if scheduled_tasks else {}, ) def _cleanup_summary( *, cleanup_runs: Sequence[Mapping[str, object]], scheduled_tasks: Sequence[Mapping[str, object]], fallback_interval_seconds: int, ) -> _CleanupSummary: task = _cleanup_task(scheduled_tasks) last_cleanup = cleanup_runs[0] if cleanup_runs else {} return _CleanupSummary( last_run=_cleanup_last_run(task, last_cleanup), next_run=task.get("next_run_at"), status=task.get("last_status") or "pending", last_error=task.get("last_error") or "", interval_seconds=task.get("interval_seconds") or fallback_interval_seconds, accounts_removed=_cleanup_accounts_removed(last_cleanup), ) def _cleanup_last_run(task: Mapping[str, object], last_cleanup: Mapping[str, object]) -> object: return task.get("last_finished_at") or last_cleanup.get("created_at") def _cleanup_accounts_removed(last_cleanup: Mapping[str, object]) -> object: return last_cleanup.get("unused_accounts_deleted", 0) 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))}/" f"{_metric_int(metrics.get('cleanup_errors_total', 0))}" ), ), ] cards = "\n".join(f""" {escape(value)}{escape(label)} """ for label, value in items) return trusted(f"""