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

+4 -101
View File
@@ -4,7 +4,7 @@ from ..ddns_models import DdnsSettings
from ..ddns_service import DdnsService
from .html_utils import SafeHtml, attr, escape, trusted
from .templates import (
_copy_button,
_copy_menu,
_copy_row,
_fritzbox_fields,
_manual_update_commands,
@@ -24,7 +24,6 @@ def _render_management_page(
account: dict[str, str | int | None],
csrf_token: str,
message: str | None = None,
challenge: dict[str, str | None] | None = None,
created_account: dict[str, str] | None = None,
) -> str:
account_for_url = {
@@ -48,7 +47,6 @@ def _render_management_page(
"""
message_html = _management_message(message)
rotated_credentials = _credentials_panel(service, created_account)
domain_claim = _management_domain_claim_panel(service, management_slug, csrf_token, challenge)
dashboard_password_panel = _dashboard_password_panel(management_slug, csrf_token, bool(account.get("dashboard_password_hash")))
return _page(
"KarlDNS management",
@@ -71,12 +69,11 @@ def _render_management_page(
<div class="col-lg-6">
<p class="eyebrow">Router settings</p>
<h2>Router update settings</h2>
<p class="section-copy">Use the Update-URL if your router has one URL field. Username and password are only for routers that require them. For your own subdomain, create a CNAME to this hostname.</p>
<p class="section-copy">Use the Update-URL if your router has one URL field. Username and password are only for routers that require them. Optional: point another DNS name at this hostname with a CNAME.</p>
{_one_time_secret_notice()}
{_secret_notice()}
<div class="d-flex gap-2 flex-wrap mt-3">
{_copy_button("Copy router fields", fritzbox_fields)}
{_copy_button("Copy CNAME", cname_record)}
<div class="copy-actions mt-3">
{_copy_menu("Copy setup values", [("Router fields", fritzbox_fields), ("CNAME", cname_record)])}
</div>
</div>
<div class="col-lg-6 d-grid gap-2">
@@ -109,7 +106,6 @@ def _render_management_page(
</div>
</div>
</section>
{domain_claim}
{dashboard_password_panel}
<section class="section">
<div class="container d-flex align-items-md-center justify-content-between gap-4 flex-column flex-md-row">
@@ -275,99 +271,6 @@ def _render_management_unlock_page(
)
def _management_domain_claim_panel(
service: DdnsService,
management_slug: str,
csrf_token: str,
challenge: dict[str, str | None] | None,
) -> SafeHtml:
create_claim = trusted(
f"""
<div class="card card-body gap-3">
<div>
<p class="eyebrow">Custom domain</p>
<h2>Use your domain</h2>
<p class="section-copy">Create a TXT record, verify it here, then create a hostname under that domain.</p>
</div>
<form method="post" action="/m/{attr(management_slug)}/domains/request" class="d-grid gap-3" aria-label="Create dashboard domain claim">
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
<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 claim</button>
</form>
</div>
"""
)
if not challenge:
return trusted(
f"""
<section class="section">
<div class="container narrow">{create_claim}</div>
</section>
"""
)
domain = str(challenge["domain"])
claim_secret = str(challenge.get("claim_secret") or "")
verification_name = service.verification_name(domain)
token = str(challenge["token"])
claim_url = service.domain_claim_url(challenge)
status = "Verified" if challenge.get("verified_at") else "Waiting for TXT"
txt_bundle = f"TXT name: {verification_name}\nTXT value: {token}"
create_hostname = _management_custom_hostname_form(management_slug, claim_secret, csrf_token) if challenge.get("verified_at") else trusted("")
return trusted(
f"""
<section class="section" id="custom-domain">
<div class="container narrow d-grid gap-4">
{create_claim}
<div class="card card-body gap-3">
<div>
<p class="eyebrow">Domain claim</p>
<h2>{escape(domain)}</h2>
<p class="section-copy">Status: {escape(status)}</p>
<p class="small text-secondary mb-0">This claim belongs to this dashboard. Other dashboards cannot use it.</p>
</div>
<div class="alert alert-info mb-0 py-2 small">TXT name and value are public. Claim secret and private claim link are secret.</div>
<div class="d-flex gap-2 flex-wrap">
{_copy_button("Copy TXT", txt_bundle)}
</div>
<div class="d-grid gap-2">
{_copy_row("TXT name", verification_name, "Public")}
{_copy_row("TXT value", token, "Public")}
{_copy_row("Claim secret", claim_secret, "Secret")}
{_copy_row("Private claim link", claim_url, "Secret")}
</div>
<form method="post" action="/m/{attr(management_slug)}/domains/verify" class="d-flex gap-2 flex-wrap" aria-label="Verify dashboard domain claim">
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
<input type="hidden" name="domain" value="{attr(domain)}">
<input type="hidden" name="claim_secret" value="{attr(claim_secret)}">
<button type="submit" class="btn btn-primary rounded-pill">Verify</button>
</form>
{create_hostname}
</div>
</div>
</section>
"""
)
def _management_custom_hostname_form(management_slug: str, claim_secret: str, csrf_token: str | None = None) -> SafeHtml:
csrf_field = f'<input type="hidden" name="csrf" value="{attr(csrf_token)}">' if csrf_token else ""
return trusted(
f"""
<form method="post" action="/m/{attr(management_slug)}/domains/hostnames" class="d-grid gap-3" aria-label="Create verified custom hostname">
{csrf_field}
<input type="hidden" name="claim_secret" value="{attr(claim_secret)}">
<label>Hostname
<input class="form-control" name="hostname" placeholder="home.example.com" autocomplete="off" autocapitalize="none" spellcheck="false" required>
</label>
<button type="submit" class="btn btn-primary rounded-pill">Create hostname</button>
</form>
"""
)
def _user_event_row(event: dict[str, str | int | None]) -> str:
created = escape(event.get("created_at") or "-")
status = escape(event.get("status") or "-")