This repository has been archived on 2026-10-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
karldns.de/router_dyndns/web/templates.py
T

271 lines
11 KiB
Python
Raw Normal View History

2026-05-20 23:55:27 +02:00
from __future__ import annotations
2026-05-21 10:05:40 +02:00
import json
2026-05-20 23:55:27 +02:00
from .html_utils import SafeHtml, attr, escape, join_html, trusted
def _top_nav(label: str | None = None, links: SafeHtml | None = None) -> SafeHtml:
account_html = ""
if label:
account_html = f'<span class="navbar-text nav-label text-truncate">{escape(label)}</span>'
if links is None:
links = trusted('<a class="nav-link px-0" href="/docs">Docs</a><a class="nav-link px-0" href="/redoc">API</a>')
2026-05-21 10:05:40 +02:00
theme_toggle = """
<button type="button" class="btn btn-sm btn-light theme-toggle" data-theme-toggle aria-label="Switch to dark theme">
<svg class="theme-icon theme-icon-moon" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M6 0a6.8 6.8 0 0 0-.8 3.2A7 7 0 0 0 12.2 10a6.8 6.8 0 0 0 3-.7A8 8 0 1 1 6 0Z"/>
</svg>
<svg class="theme-icon theme-icon-sun" aria-hidden="true" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 11.2A3.2 3.2 0 1 0 8 4.8a3.2 3.2 0 0 0 0 6.4Zm0-9.8a.7.7 0 0 1-.7-.7V.2a.7.7 0 0 1 1.4 0v.5a.7.7 0 0 1-.7.7Zm0 14.6a.7.7 0 0 1-.7-.7v-.5a.7.7 0 1 1 1.4 0v.5a.7.7 0 0 1-.7.7ZM1.4 8a.7.7 0 0 1-.7.7H.2a.7.7 0 0 1 0-1.4h.5a.7.7 0 0 1 .7.7Zm14.6 0a.7.7 0 0 1-.7.7h-.5a.7.7 0 1 1 0-1.4h.5a.7.7 0 0 1 .7.7ZM3.3 3.3a.7.7 0 0 1-1 0L2 3a.7.7 0 0 1 1-1l.3.3a.7.7 0 0 1 0 1Zm10.7 10.7a.7.7 0 0 1-1 0l-.3-.3a.7.7 0 1 1 1-1l.3.3a.7.7 0 0 1 0 1Zm-10.7-1a.7.7 0 0 1 0 1l-.3.3a.7.7 0 1 1-1-1l.3-.3a.7.7 0 0 1 1 0ZM14 2a.7.7 0 0 1 0 1l-.3.3a.7.7 0 1 1-1-1L13 2a.7.7 0 0 1 1 0Z"/>
</svg>
</button>
"""
2026-05-20 23:55:27 +02:00
return trusted(
f"""
<a class="skip-link" href="#main">Skip to content</a>
<nav class="navbar navbar-expand-sm site-nav" aria-label="Main navigation">
<div class="container">
2026-05-21 10:05:40 +02:00
<a class="navbar-brand brand" href="/"><img src="/logo-nav.webp?v=3" alt="" width="28" height="28" decoding="async">KarlDNS</a>
2026-05-20 23:55:27 +02:00
<div class="navbar-nav site-nav-actions ms-auto flex-row align-items-center justify-content-end gap-2 gap-sm-3">
{links}
{account_html}
{theme_toggle}
</div>
</div>
</nav>
"""
)
def _copy_row(label: str, value: str, sensitivity: str | None = None) -> 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'<span class="badge {badge_class} rounded-pill">{safe_sensitivity}</span>'
value_field = (
f'<input class="form-control copy-value text-truncate" type="password" value="{safe_value}" readonly data-secret-input>'
if is_secret
else f'<code class="form-control copy-value text-truncate">{safe_value}</code>'
)
reveal_button = (
f'<button type="button" class="btn btn-outline-secondary" data-secret-toggle aria-label="Show {safe_label}">View</button>'
if is_secret
else ""
)
return trusted(
f"""
<div class="input-group copy-field">
<span class="input-group-text copy-label"><span>{safe_label}</span>{badge}</span>
{value_field}
{reveal_button}
<button type="button" class="btn btn-outline-secondary" data-copy="{safe_value}" aria-label="Copy {safe_label}">Copy</button>
</div>
"""
)
def _copy_button(label: str, value: str) -> SafeHtml:
safe_label = escape(label)
safe_value = attr(value)
return trusted(f'<button type="button" class="btn btn-outline-primary rounded-pill" data-copy="{safe_value}">{safe_label}</button>')
def _manual_update_commands(update_url: str) -> str:
update_base = update_url.split("?", 1)[0]
return "\n".join(
[
f"curl -sS '{update_base}?myip=203.0.113.10'",
f"curl -sS '{update_base}?myipv6=2001:db8::10'",
2026-05-21 10:05:40 +02:00
f"curl -sS '{update_base}' # use request IP",
2026-05-20 23:55:27 +02:00
]
)
def _manual_update_panel(commands: str) -> SafeHtml:
safe_commands = escape(commands)
return trusted(
f"""
<div class="card card-body gap-3 mt-4">
<div>
<h3>Manual updates</h3>
2026-05-21 10:05:40 +02:00
<p class="section-copy">FRITZ!Box fills in the IP placeholders automatically. Scripts can use curl. If no IP is sent, KarlDNS uses the request IP.</p>
2026-05-20 23:55:27 +02:00
</div>
<pre class="manual-code mb-0"><code>{safe_commands}</code></pre>
2026-05-21 10:05:40 +02:00
{_copy_button("Copy curl", commands)}
2026-05-20 23:55:27 +02:00
</div>
"""
)
def _fritzbox_fields(
update_url: str,
hostname: str,
username: str,
password: str,
management_url: str | None = None,
) -> str:
lines = [
f"Update-URL: {update_url}",
f"Domainnamen: {hostname}",
f"CNAME target: {hostname}",
f"Benutzername: {username}",
f"Kennwort: {password}",
]
if management_url:
2026-05-21 10:05:40 +02:00
lines.append(f"Private dashboard: {management_url}")
2026-05-20 23:55:27 +02:00
return "\n".join(lines)
def _secret_notice() -> SafeHtml:
return trusted(
"""
<div class="alert alert-info my-3 py-2 small" role="note">
2026-05-21 10:05:40 +02:00
<strong>Public:</strong> Domain name and CNAME target can be shared.
<strong>Secret:</strong> Update URLs, router password, dashboard link, and claim links can update or manage this hostname.
Secrets are hidden on screen. Copy still copies the real value.
2026-05-20 23:55:27 +02:00
</div>
"""
)
def _one_time_secret_notice() -> SafeHtml:
return trusted(
"""
<div class="alert alert-warning my-3 py-2 small" role="alert">
2026-05-21 10:05:40 +02:00
Copy the router password and full update URL now. They are only shown when created or rotated.
2026-05-20 23:55:27 +02:00
</div>
"""
)
def _message_band(message: str | None) -> SafeHtml:
if not message:
return trusted("")
return trusted(
f"""
<div class="alert alert-warning mb-0 rounded-0 border-start-0 border-end-0" role="status">
<div class="container">{escape(message)}</div>
</div>
"""
)
def _site_footer() -> SafeHtml:
return trusted(
"""
<footer class="site-footer border-top">
<div class="container d-flex align-items-center justify-content-between gap-3 flex-column flex-md-row">
<div class="small text-secondary">Made by Karl</div>
<nav class="nav justify-content-center gap-3" aria-label="Footer links">
<a class="nav-link p-0 small text-secondary" href="https://github.com/StasonJatham/karldns.de" rel="noopener">GitHub</a>
<a class="nav-link p-0 small text-secondary" href="https://karl.fail" rel="noopener">karl.fail</a>
<a class="nav-link p-0 small text-secondary" href="https://karlcom.de" rel="noopener">karlcom.de</a>
<a class="nav-link p-0 small text-secondary" href="/docs">API docs</a>
<a class="nav-link p-0 small text-secondary" href="/redoc">ReDoc</a>
<a class="nav-link p-0 small text-secondary" href="/privacy">Legal & privacy</a>
</nav>
</div>
</footer>
"""
)
2026-05-21 10:05:40 +02:00
def _page(
title: str,
body: SafeHtml,
extra_styles: SafeHtml | None = None,
extra_scripts: SafeHtml | None = None,
description: str = "KarlDNS is a small self-hosted Dynamic DNS service for FRITZ!Box routers, Linux cron jobs, and home networks.",
preload_logo: bool = False,
canonical_url: str | None = None,
image_url: str | None = None,
image_alt: str | None = None,
structured_data: dict[str, object] | None = None,
) -> str:
2026-05-20 23:55:27 +02:00
if not isinstance(body, SafeHtml):
raise TypeError("body must be a SafeHtml fragment built by a template helper")
2026-05-21 10:05:40 +02:00
extra_styles_html = extra_styles or trusted("")
extra_scripts_html = extra_scripts or trusted("")
preload_logo_html = trusted('<link rel="preload" as="image" href="/logo.webp?v=3" type="image/webp">') if preload_logo else trusted("")
canonical_html = trusted(f'<link rel="canonical" href="{attr(canonical_url)}">') if canonical_url else trusted("")
social_url = canonical_url or ""
social_image = image_url or ""
social_html = trusted("")
if social_url:
social_card = "summary_large_image" if social_image else "summary"
social_image_alt = image_alt or title
social_image_html = f"""
<meta property="og:image" content="{attr(social_image)}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{attr(social_image_alt)}">
<meta name="twitter:image" content="{attr(social_image)}">
<meta name="twitter:image:alt" content="{attr(social_image_alt)}">
""" if social_image else ""
social_html = trusted(f"""
<meta property="og:type" content="website">
<meta property="og:title" content="{attr(title)}">
<meta property="og:description" content="{attr(description)}">
<meta property="og:url" content="{attr(social_url)}">
{social_image_html}
<meta name="twitter:card" content="{attr(social_card)}">
<meta name="twitter:title" content="{attr(title)}">
<meta name="twitter:description" content="{attr(description)}">
""")
structured_data_html = trusted("")
if structured_data:
structured_data_html = trusted(
'<script type="application/ld+json">'
+ escape(json.dumps(structured_data, ensure_ascii=False, separators=(",", ":")))
+ "</script>"
)
2026-05-20 23:55:27 +02:00
return f"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff">
2026-05-21 10:05:40 +02:00
<meta name="description" content="{attr(description)}">
{canonical_html}
{social_html}
2026-05-20 23:55:27 +02:00
<title>{escape(title)}</title>
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
2026-05-21 10:05:40 +02:00
{preload_logo_html}
<script src="/theme.js?v=2"></script>
<script src="/clipboard.js?v=2" defer></script>
{extra_scripts_html}
<link href="/bootstrap-subset.css?v=3" rel="stylesheet">
<link href="/app.css?v=2" rel="stylesheet">
{extra_styles_html}
{structured_data_html}
2026-05-20 23:55:27 +02:00
</head>
<body>{body}{_site_footer()}</body>
</html>
"""
__all__ = [
"SafeHtml",
"_copy_button",
"_copy_row",
"_fritzbox_fields",
"_manual_update_commands",
"_manual_update_panel",
"_message_band",
"_one_time_secret_notice",
"_page",
"_secret_notice",
"_top_nav",
"join_html",
"trusted",
]