from __future__ import annotations from ..services.ddns import DdnsService from ..settings import DdnsSettings from .components import ( _code_window, _copy_button, _copy_row, _event_label, _fritzbox_fields, _manual_update_commands, _secret_notice, ) from .html_utils import SafeHtml, attr, escape, trusted from .layout import _page, _top_nav def _render_management_page( service: DdnsService, settings: DdnsSettings, management_slug: str, account: dict[str, str | int | None], csrf_token: str, message: str | None = None, request_ip: str | None = None, ) -> str: account_for_url = { "hostname": str(account["hostname"]), "update_slug": str(account["update_slug"]), "management_slug": management_slug, } update_url = service.fritz_update_url(account_for_url) hostname = escape(account["hostname"]) ipv4 = escape(account.get("ipv4") or "-") ipv6 = escape(account.get("ipv6") or "-") updated = escape(account.get("updated_at") or "Never") fritzbox_fields = _fritzbox_fields(update_url, hostname) cname_record = f"your-subdomain.example.com. CNAME {hostname}." manual_commands = _manual_update_commands(update_url, request_ip) history_rows = "\n".join(_user_event_row(event) for event in service.store.list_update_events_for_hostname(str(account["hostname"]), 10)) or """ No router updates yet. """ message_html = _management_message(message) dashboard_password_panel = _dashboard_password_panel(management_slug, csrf_token, bool(account.get("dashboard_password_hash"))) return _page( "KarlDNS management", trusted(f"""
{_top_nav()}

Dashboard

{hostname}

Private management link. Keep it safe.

Home
{message_html}

Router settings

Update endpoint

Paste the Update-URL into your router. Use the hostname as Domainnamen, or point your own subdomain at it with a CNAME.

{_secret_notice()}
{_copy_row("Update-URL:", update_url, "Secret")} {_copy_row("Domainnamen:", hostname, "Public")} {_copy_row("CNAME target:", hostname, "Public")}
{_copy_button("Router fields", fritzbox_fields)} {_copy_button("Copy CNAME", cname_record, style="outline-secondary")}

Current address

Status

IPv4
{ipv4}
IPv6
{ipv6}
Updated
{updated}

Manual updates

curl examples

FRITZ!Box fills IP placeholders automatically. Scripts can send an IP explicitly or omit it and let KarlDNS use the request IP.

{_code_window(manual_commands, copy_label="manual update commands", class_name="manual-code")}
{dashboard_password_panel}

History

Router updates

Recent IP changes and failed updates for this hostname.

{history_rows}
TimeStatusIPv4IPv6CountDetail

Delete

Delete hostname

Remove this hostname and its DNS records. The update URL will stop working.

"""), extra_styles=trusted(''), ) def _render_pending_management_page( service: DdnsService, settings: DdnsSettings, management_slug: str, account: dict[str, str | int | None], csrf_token: str, message: str | None = None, request_ip: str | None = None, ) -> str: raw_hostname = str(account["hostname"]) account_for_url = { "hostname": raw_hostname, "update_slug": str(account["update_slug"]), "management_slug": management_slug, } update_url = service.fritz_update_url(account_for_url) dashboard_url = service.dashboard_url({"management_slug": management_slug}) hostname = escape(raw_hostname) message_html = _management_message(message) cleanup_hours = settings.cleanup_pending_account_hours cleanup_text = f"{cleanup_hours} hours" if cleanup_hours != 24 else "24 hours" manual_commands = _manual_update_commands(update_url, request_ip) return _page( "KarlDNS setup", trusted(f"""
{_top_nav()}

Pending setup

{hostname}

Save these values, then register the hostname. Pending setups are cleaned up after {escape(cleanup_text)}.

{message_html}

Save

Your values.

Save the update URL and dashboard link before you register. After registration, the dashboard hides secrets by default.

Public: Domainnamen. Secret: Update-URL and private dashboard.

{_copy_row("Update-URL:", update_url, "Secret", mask_secret=False)} {_copy_row("Domainnamen:", raw_hostname, "Public")} {_copy_row("Private dashboard:", dashboard_url, "Secret", mask_secret=False)}

FRITZ!Box

Router setup.

Paste this exact URL into the FRITZ!Box Update-URL field. Use {hostname} as Domainnamen. No KarlDNS username or password is needed; if your router requires them, enter any placeholder.

{_code_window(update_url, copy_label="FRITZ!Box update URL", class_name="mt-3")}

Manual updates.

Scripts can send the IP explicitly, or omit it and let KarlDNS use the request IP. Visible examples use your public IP when KarlDNS or your browser can detect it.

{_code_window(manual_commands, copy_label="manual update commands", class_name="manual-code mt-3")}

Use your own DNS name.

Create a CNAME at your DNS provider that points to {hostname}. This is the same value as Domainnamen.

"""), ) def _management_message(message: str | None) -> SafeHtml: if not message: return trusted("") return trusted( f"""
{escape(message)}
""" ) def _dashboard_password_panel(management_slug: str, csrf_token: str, password_enabled: bool) -> SafeHtml: password_status = "Enabled" if password_enabled else "Not enabled" password_action = "Change password" if password_enabled else "Set password" remove_form = "" if password_enabled: remove_form = f"""
""" current_password_field = "" if password_enabled: current_password_field = """
""" return trusted( f"""

Dashboard password

Password protection

Status: {escape(password_status)}. This password protects only this dashboard. Router updates still work.

{current_password_field}
{remove_form}
""" ) def _render_management_unlock_page( management_slug: str, account: dict[str, str | int | None], csrf_token: str, message: str | None = None, ) -> str: hostname = escape(account["hostname"]) message_html = "" if message: message_html = f'' return _page( "Unlock KarlDNS management", trusted(f"""
{_top_nav()}

Dashboard locked

{hostname}

Enter the dashboard password for this hostname.

{message_html}
"""), ) def _user_event_row(event: dict[str, str | int | None]) -> str: created = escape(event.get("created_at") or "-") raw_status = str(event.get("status") or "") status = escape(_event_label(raw_status)) badge_class = attr(_event_badge_class(raw_status)) ipv4 = escape(event.get("ipv4") or "-") ipv6 = escape(event.get("ipv6") or "-") count = escape(event.get("count") or 1) detail = escape(event.get("detail") or "-") return f'{created}{status}{ipv4}{ipv6}{count}{detail}' def _event_badge_class(status: str) -> str: if status in {"updated", "dashboard_hostname_activated", "unchanged"}: return "status-badge-ok" if "failed" in status or "failure" in status: return "status-badge-error" return "status-badge-neutral"