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

+15 -111
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
from ..ddns_service import DdnsService
from .html_utils import SafeHtml, attr, escape, trusted
from .html_utils import SafeHtml, escape, trusted
from .templates import (
_copy_button,
_code_window,
_copy_menu,
_copy_row,
_fritzbox_fields,
_manual_update_commands,
@@ -18,7 +19,6 @@ def _linux_installer_section() -> SafeHtml:
system_uninstall = "curl -fsSL https://karldns.de/uninstall.sh | sudo sh"
user_install = "curl -fsSL https://karldns.de/install.sh | sh -s -- --mode user"
clean_uninstall = "curl -fsSL https://karldns.de/uninstall.sh | sh -s -- --mode user --delete-config"
all_commands = "\n".join([system_install, system_uninstall, user_install, clean_uninstall])
return trusted(
f"""
<section class="section" id="linux-installer">
@@ -29,15 +29,16 @@ def _linux_installer_section() -> SafeHtml:
<p class="section-copy mx-auto">Run this on a Linux box that is always on. It creates a hostname, installs the updater, schedules it, and prints your private dashboard link.</p>
</div>
<div class="card card-body gap-3">
<div class="terminal-card">
<code>{escape(system_install)}</code>
<code>{escape(system_uninstall)}</code>
<div>
<h3>Install</h3>
<p class="section-copy">System install for most Linux servers.</p>
</div>
<div class="d-flex gap-2 flex-wrap">
{_copy_button("Copy install", system_install)}
{_copy_button("Copy uninstall", system_uninstall)}
{_copy_button("Copy all commands", all_commands)}
{_code_window(system_install, copy_label="install command")}
<div>
<h3>Uninstall</h3>
<p class="section-copy">Stops the updater and leaves your private URLs on disk.</p>
</div>
{_code_window(system_uninstall, copy_label="uninstall command")}
<div class="alert alert-info my-0 py-2 small" role="note">
Uninstall keeps your private URLs by default. Use <code>--delete-config</code> only when you want to remove everything.
</div>
@@ -45,67 +46,8 @@ def _linux_installer_section() -> SafeHtml:
<h3>User install</h3>
<p class="section-copy">Use this when you do not want a system-wide install.</p>
</div>
<div class="terminal-card">
<code>{escape(user_install)}</code>
<code>{escape(clean_uninstall)}</code>
</div>
<div class="d-flex gap-2 flex-wrap">
{_copy_button("Copy user install", user_install)}
{_copy_button("Copy clean user uninstall", clean_uninstall)}
</div>
</div>
</div>
</section>
"""
)
def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] | None, message: str | None) -> SafeHtml:
if not challenge:
return trusted("")
domain = attr(challenge["domain"])
token = str(challenge["token"])
claim_secret = attr(challenge.get("claim_secret") or "")
verification_name = service.verification_name(str(challenge["domain"]))
claim_url = service.domain_claim_url(challenge)
status = escape(message or "Add this TXT record at your DNS provider, then check again. DNS can take a few minutes.")
txt_bundle = f"TXT name: {verification_name}\nTXT value: {token}"
challenge_html = trusted(
f"""
<div class="col-lg-5">
<div class="card card-body h-100 gap-3">
<span class="badge text-bg-primary rounded-circle align-self-start">1</span>
<h3>Add the TXT record</h3>
<p class="small text-secondary mb-0 mt-2">{status}</p>
<div class="alert alert-info mb-0 py-2 small">TXT name and value are public. The private claim link is secret.</div>
{_copy_button("Copy TXT", txt_bundle)}
{_copy_row("TXT name", verification_name, "Public")}
{_copy_row("TXT value", token, "Public")}
{_copy_row("Private claim link", claim_url, "Secret")}
<form method="post" action="/verify-domain" class="mt-3" aria-label="Check DNS TXT verification">
<input type="hidden" name="domain" value="{domain}">
<input type="hidden" name="claim_secret" value="{claim_secret}">
<button type="submit" class="btn btn-primary rounded-pill">Check DNS record</button>
</form>
</div>
</div>
"""
)
create_form = _custom_hostname_form(claim_secret) if challenge.get("verified_at") else _locked_custom_hostname_card()
return trusted(
f"""
<section class="section" id="custom-domain">
<div class="container narrow">
<div class="text-center">
<p class="eyebrow">Custom domain</p>
<h2>Verify your domain</h2>
<p class="section-copy mx-auto">Use this when KarlDNS should update records inside your domain. For a simpler setup, create a KarlDNS hostname and point a CNAME to it.</p>
</div>
<div class="row g-3 mt-4 justify-content-center">
{challenge_html}
{create_form}
{_code_window(user_install, copy_label="user install command")}
{_copy_menu("More copy options", [("Clean user uninstall", clean_uninstall)])}
</div>
</div>
</section>
@@ -134,10 +76,8 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
<p class="section-copy">Use the Update-URL directly. Username and password are only for routers that require separate fields.</p>
{_one_time_secret_notice()}
{_secret_notice()}
<div class="d-flex gap-2 flex-wrap my-3">
{_copy_button("Copy router fields", fritzbox_fields)}
{_copy_button("Copy full URL", credentialed_update_url)}
{_copy_button("Copy CNAME", cname_record)}
<div class="copy-actions my-3">
{_copy_menu("Copy setup values", [("Router fields", fritzbox_fields), ("Full update URL", credentialed_update_url), ("CNAME", cname_record)])}
</div>
<div class="d-grid gap-2">
{_copy_row("Update-URL:", update_url, "Secret")}
@@ -153,39 +93,3 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
</section>
"""
)
def _custom_hostname_form(claim_secret: str) -> SafeHtml:
return trusted(
f"""
<div class="col-lg-5">
<div class="card card-body h-100 gap-3">
<span class="badge text-bg-primary rounded-circle align-self-start">2</span>
<h3>Create hostname</h3>
<form method="post" action="/accounts" class="d-grid gap-3" aria-label="Create custom-domain router credentials">
<input type="hidden" name="mode" value="custom">
<input type="hidden" name="claim_secret" value="{claim_secret}">
<label>Verified hostname
<input class="form-control" name="hostname" placeholder="home.example.com" autocomplete="off" autocapitalize="none" spellcheck="false" required>
</label>
<p class="section-copy mb-0">Router fields are created after this step.</p>
<button type="submit" class="btn btn-primary rounded-pill">Create hostname</button>
</form>
</div>
</div>
"""
)
def _locked_custom_hostname_card() -> SafeHtml:
return trusted(
"""
<div class="col-lg-5">
<div class="card card-body h-100 gap-3 opacity-75">
<span class="badge text-bg-primary rounded-circle align-self-start">2</span>
<h3>Create hostname</h3>
<p class="small text-secondary mb-0 mt-2">Available after KarlDNS can see the TXT record.</p>
</div>
</div>
"""
)