from __future__ import annotations from ipaddress import ip_address from .html_utils import SafeHtml, attr, escape, trusted from .icons import _svg_icon _EVENT_LABELS = { "admin_hostname_deleted": "hostname deleted by admin", "admin_management_url_rotated": "dashboard link rotated by admin", "admin_update_url_rotated": "update URL rotated by admin", "dashboard_hostname_activated": "hostname registered", "dashboard_hostname_deleted": "hostname deleted", "dashboard_password_changed": "dashboard password changed", "dashboard_password_enabled": "dashboard password enabled", "dashboard_password_removed": "dashboard password removed", "dashboard_update_url_rotated": "update URL rotated", "dns_delete_failed": "DNS delete failed", "dns_failed": "DNS update failed", "dns_reconcile_failed": "DNS reconciliation failed", "unchanged": "unchanged", "updated": "updated", } def _event_label(status: object) -> str: value = str(status or "-") return _EVENT_LABELS.get(value, value.replace("_", " ")) def _copy_row(label: str, value: str, sensitivity: str | None = None, *, mask_secret: bool = True) -> SafeHtml: safe_label = escape(label) safe_value = escape(value) badge = "" is_secret = bool(sensitivity and sensitivity.lower() == "secret") if sensitivity: safe_sensitivity = escape(sensitivity) badge_class = "text-bg-warning" if is_secret else "text-bg-secondary" badge = f'{safe_sensitivity}' should_mask = is_secret and mask_secret value_field = ( f'' if should_mask else f'{safe_value}' ) reveal_button = ( f'' if should_mask else "" ) return trusted( f"""
{safe_label}{badge} {value_field} {reveal_button}
""" ) def _copy_button(label: str, value: str, *, style: str = "outline-primary") -> SafeHtml: safe_label = escape(label) safe_value = attr(value) safe_style = attr(style) return trusted( f'' ) def _copy_icon_button(label: str, value: str) -> SafeHtml: safe_label = escape(label) safe_value = attr(value) return trusted( f'' ) def _copy_menu(label: str, items: list[tuple[str, str]]) -> SafeHtml: safe_label = escape(label) buttons = [] for item_label, item_value in items: buttons.append( f'' ) return trusted( f"""
{_svg_icon("more", "more-icon")}
""" ) def _code_window(commands: str, *, copy_label: str = "code", class_name: str = "", copy_value: str | None = None) -> SafeHtml: safe_class = f" {attr(class_name)}" if class_name else "" code_lines = "\n".join(f"{escape(line)}" for line in commands.splitlines()) copy_value = copy_value if copy_value is not None else _code_copy_value(commands) return trusted( f"""
{_copy_icon_button(copy_label, copy_value)} {code_lines}
""" ) def _code_copy_value(commands: str) -> str: lines = commands.splitlines() prompt_lines = [line[2:] for line in lines if line.startswith("$ ")] if prompt_lines: return "\n".join(prompt_lines) return "\n".join(line[2:] if line.startswith("$ ") else line for line in lines) def _example_update_ips(request_ip: str | None = None) -> tuple[str, str]: ipv4 = "203.0.113.10" ipv6 = "2001:db8::10" if not request_ip: return ipv4, ipv6 try: parsed = ip_address(request_ip.strip()) except ValueError: return ipv4, ipv6 if not parsed.is_global: return ipv4, ipv6 if parsed.version == 4: return str(parsed), ipv6 return ipv4, str(parsed) def _manual_update_commands(update_url: str, request_ip: str | None = None) -> str: update_base = update_url.split("?", 1)[0] ipv4, ipv6 = _example_update_ips(request_ip) return "\n".join( [ f"curl -sS '{update_base}?myip={ipv4}'", f"curl -sS '{update_base}?myipv6={ipv6}'", f"curl -sS '{update_base}' # use request IP", ] ) def _manual_update_panel(commands: str) -> SafeHtml: return trusted( f"""

Manual updates

FRITZ!Box fills in the IP placeholders automatically. Scripts can use curl. If no IP is sent, KarlDNS uses the request IP.

{_code_window(commands, copy_label="manual update commands", class_name="manual-code")}
""" ) def _fritzbox_fields( update_url: str, hostname: str, management_url: str | None = None, ) -> str: lines = [ f"Update-URL: {update_url}", f"Domainnamen: {hostname}", f"CNAME target: {hostname}", ] if management_url: lines.append(f"Private dashboard: {management_url}") return "\n".join(lines) def _secret_notice(*, include_registration: bool = False) -> SafeHtml: secret_items = "Update URL, register URL, and dashboard link" if include_registration else "Update URL and dashboard link" return trusted( f"""
Public: Hostname and CNAME target can be shared. Secret: {escape(secret_items)} can update or manage this hostname. Secrets are hidden on screen. Copy still copies the real value.
""" ) def _message_band(message: str | None) -> SafeHtml: if not message: return trusted("") return trusted( f"""
{escape(message)}
""" )