Simplify DDNS flow and polish UI

This commit is contained in:
StasonJatham committed 2026-05-21 14:18:39 +02:00
1 parent 5beee0e5c6
commit 5a90bd9d87
42 files changed
+587 -1418

No files matched your search

+31 -35
View File
@@ -3,8 +3,8 @@ from __future__ import annotations
from ..ddns_models import DdnsSettings
from ..ddns_service import DdnsService
from .html_utils import escape, trusted
from .templates import _message_band, _page, _top_nav
from .templates_public_sections import _credentials_panel, _custom_domain_flow, _linux_installer_section
from .templates import _code_window, _message_band, _page, _top_nav
from .templates_public_sections import _credentials_panel, _linux_installer_section
def _render_public_home(
@@ -12,15 +12,24 @@ def _render_public_home(
settings: DdnsSettings,
created_account: dict[str, str] | None,
message: str | None,
challenge: dict[str, str | None] | None = None,
) -> str:
base_url = settings.public_base_url.rstrip("/")
suffix = escape(settings.hostname_suffix or "your DynDNS domain")
hostname_suffix = settings.hostname_suffix or "your KarlDNS suffix"
suffix = escape(hostname_suffix)
demo_update_url = escape(f"{base_url}/u/<secret>?myip=<ipaddr>")
cname_example = f"home.example.com. CNAME your-name.{hostname_suffix}."
create_demo = "$ curl -sS 'https://karldns.de/u/<secret>?myip=203.0.113.10'\ngood 203.0.113.10"
manual_demo = "\n".join(
[
"# explicit IPv4",
"curl -sS 'https://karldns.de/u/<secret>?myip=203.0.113.10'",
"# source IP fallback",
"curl -sS 'https://karldns.de/u/<secret>'",
]
)
created_html = _credentials_panel(service, created_account)
message_html = _message_band(message) if message and not challenge else ""
challenge_html = _custom_domain_flow(service, challenge, message)
description = "KarlDNS is a free self-hosted Dynamic DNS service for FRITZ!Box routers, custom domains, Linux cron jobs, and home networks."
message_html = _message_band(message) if message else ""
description = "KarlDNS is a free self-hosted Dynamic DNS service for FRITZ!Box routers, generated KarlDNS hostnames, Linux cron jobs, and home networks."
return _page(
"KarlDNS",
trusted(f"""
@@ -33,7 +42,7 @@ def _render_public_home(
<div class="install-pill mx-auto">
<code>{demo_update_url}</code>
</div>
<p class="lead">KarlDNS gives you a private DynDNS update URL under {suffix}. Add it to your router, or use it from a script.</p>
<p class="lead">KarlDNS gives you a generated DynDNS hostname under {suffix}. Add the private update URL to your router, or use it from a script.</p>
<div class="d-flex gap-2 flex-wrap justify-content-center mt-2">
<a class="btn btn-primary rounded-pill" href="#create">Create hostname</a>
<a class="btn btn-outline-primary rounded-pill" href="#manual-updates">Manual updates</a>
@@ -47,12 +56,12 @@ def _render_public_home(
<div class="text-center mb-4">
<p class="eyebrow">Hostname</p>
<h2>No account needed.</h2>
<p class="section-copy mx-auto">Use a KarlDNS hostname now, or verify your own domain when you need it.</p>
<p class="section-copy mx-auto">Create a KarlDNS hostname. Use it directly, or point your own DNS name at it with a CNAME.</p>
</div>
<div class="row g-4 align-items-stretch">
<div class="col-lg-6">
<div class="card card-body h-100 gap-3">
<p class="eyebrow">KarlDNS domain</p>
<p class="eyebrow">KarlDNS hostname</p>
<h3>Create a hostname</h3>
<p class="section-copy mb-0">You get a hostname, a private update URL, and router fields if your router needs them.</p>
<form method="post" action="/magic" class="d-grid gap-3" aria-label="Create a generated DynDNS hostname">
@@ -62,38 +71,25 @@ def _render_public_home(
</div>
<div class="col-lg-6">
<div class="card card-body h-100 gap-3">
<p class="eyebrow">Use your own domain</p>
<h3>Verify your domain</h3>
<p class="section-copy mb-0">Add one TXT record first. KarlDNS only updates domains it can verify.</p>
<form method="post" action="/request-domain" class="d-grid gap-3" aria-label="Request a custom domain TXT challenge">
<label>Domain
<input class="form-control" name="domain" placeholder="example.com" autocomplete="off" autocapitalize="none" spellcheck="false" required>
</label>
<button type="submit" class="btn btn-primary rounded-pill">Create TXT record</button>
</form>
<p class="eyebrow">Optional DNS alias</p>
<h3>Add a CNAME</h3>
<p class="section-copy mb-0">After creation, add a CNAME at your DNS provider:</p>
{_code_window(cname_example, copy_label="CNAME example")}
<p class="small text-secondary mb-0">KarlDNS updates only the generated hostname. The external DNS name follows it through CNAME.</p>
</div>
</div>
</div>
<div class="terminal-card mt-4" aria-hidden="true">
<code>$ curl -sS 'https://karldns.de/u/&lt;secret&gt;?myip=203.0.113.10'</code>
<code>good 203.0.113.10</code>
</div>
{_code_window(create_demo, copy_label="update example", class_name="mt-4")}
</div>
</section>
<section class="section" id="manual-updates">
<div class="container narrow text-center">
<h2>Update from a router or script.</h2>
<p class="lead mx-auto mt-3">Works with FRITZ!Box, curl, cron, Home Assistant, OpenWrt, pfSense, OPNsense, UniFi, MikroTik, and custom tools.</p>
<div class="terminal-card text-start mt-4">
<code># explicit IPv4</code>
<code>curl -sS 'https://karldns.de/u/&lt;secret&gt;?myip=203.0.113.10'</code>
<code># source IP fallback</code>
<code>curl -sS 'https://karldns.de/u/&lt;secret&gt;'</code>
</div>
{_code_window(manual_demo, copy_label="manual update examples", class_name="text-start mt-4")}
</div>
</section>
{_linux_installer_section()}
{challenge_html}
</main>
"""),
description=description,
@@ -114,7 +110,7 @@ def _render_public_home(
"featureList": [
"Dynamic DNS for FRITZ!Box routers",
"Accountless private update URLs",
"Custom domain TXT verification",
"CNAME target for external DNS names",
"Linux cron updater",
],
},
@@ -122,7 +118,7 @@ def _render_public_home(
def _render_privacy_page(settings: DdnsSettings) -> str:
base_url = settings.public_base_url.rstrip("/")
description = "Privacy and legal information for KarlDNS, including stored hostnames, update links, DNS verification records, IP history, and cleanup behavior."
description = "Privacy and legal information for KarlDNS, including stored hostnames, update links, IP history, and cleanup behavior."
return _page(
"Legal and Privacy",
trusted(f"""
@@ -139,15 +135,15 @@ def _render_privacy_page(settings: DdnsSettings) -> str:
<div class="container narrow d-grid gap-4">
<div>
<h2>Data processed</h2>
<p class="section-copy">KarlDNS stores hostnames, update links, protected dashboard tokens, protected router passwords, TXT verification records, recent IP history, timestamps, and service events.</p>
<p class="section-copy">KarlDNS stores hostnames, update links, protected dashboard tokens, protected router passwords, recent IP history, timestamps, and service events.</p>
</div>
<div>
<h2>Secrets</h2>
<p class="section-copy">Dashboard links, update URLs, router passwords, full update URLs, and claim links are secrets. Anyone with them may update or manage the hostname. Passwords and dashboard lookup values are not stored as readable text.</p>
<p class="section-copy">Dashboard links, update URLs, router passwords, and full update URLs are secrets. Anyone with them may update or manage the hostname. Passwords and dashboard lookup values are not stored as readable text.</p>
</div>
<div>
<h2>Logs and retention</h2>
<p class="section-copy">KarlDNS keeps recent update events for troubleshooting. Cleanup removes abandoned setup records and unused hostnames. Server logs may also include IP addresses and request details.</p>
<p class="section-copy">KarlDNS keeps recent update events for troubleshooting. Cleanup removes unused hostnames. Server logs may also include IP addresses and request details.</p>
</div>
<div>
<h2>Contact</h2>