Add grouped clipboard actions

This commit is contained in:
StasonJatham committed 2026-05-20 19:08:06 +02:00
1 parent 73bc201511
commit 7ba7610c41
2 files changed
+45

No files matched your search

+39
View File
@@ -719,6 +719,7 @@ def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] |
verification_name = service.verification_name(str(challenge["domain"])) verification_name = service.verification_name(str(challenge["domain"]))
claim_url = service.domain_claim_url(challenge) claim_url = service.domain_claim_url(challenge)
status = html.escape(message or "Add this TXT record at your DNS provider, then check when it has propagated.") status = html.escape(message or "Add this TXT record at your DNS provider, then check when it has propagated.")
txt_bundle = f"TXT name: {verification_name}\nTXT value: {token}"
challenge_html = f""" challenge_html = f"""
<div class="col-md-4"> <div class="col-md-4">
<div class="card card-body h-100 gap-3"> <div class="card card-body h-100 gap-3">
@@ -726,6 +727,7 @@ def _custom_domain_flow(service: DdnsService, challenge: dict[str, str | None] |
<h3>Add the TXT record</h3> <h3>Add the TXT record</h3>
<p class="small text-secondary mb-0 mt-2">{status}</p> <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 DNS records. The private claim link is secret.</div> <div class="alert alert-info mb-0 py-2 small">TXT name and value are public DNS records. The private claim link is secret.</div>
{_copy_button("Copy TXT record", txt_bundle)}
{_copy_row("TXT name", verification_name, "Public")} {_copy_row("TXT name", verification_name, "Public")}
{_copy_row("TXT value", token, "Public")} {_copy_row("TXT value", token, "Public")}
{_copy_row("Private claim link", claim_url, "Secret")} {_copy_row("Private claim link", claim_url, "Secret")}
@@ -808,6 +810,8 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
password = created_account["password"] password = created_account["password"]
update_url = service.fritz_update_url(created_account) update_url = service.fritz_update_url(created_account)
management_url = service.magic_management_url(created_account) management_url = service.magic_management_url(created_account)
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
fritzbox_fields = _fritzbox_fields(update_url, hostname, username, password, management_url)
return f""" return f"""
<section class="section success-section"> <section class="section success-section">
<div class="container"> <div class="container">
@@ -815,6 +819,10 @@ def _credentials_panel(service: DdnsService, created_account: dict[str, str] | N
<h2>{html.escape(hostname)}</h2> <h2>{html.escape(hostname)}</h2>
<p class="section-copy">Save this now. The password and private status page are only shown on this screen. To use your own DNS name, create a CNAME from that name to this hostname.</p> <p class="section-copy">Save this now. The password and private status page are only shown on this screen. To use your own DNS name, create a CNAME from that name to this hostname.</p>
{_secret_notice()} {_secret_notice()}
<div class="d-flex gap-2 flex-wrap my-3">
{_copy_button("Copy FRITZ!Box fields", fritzbox_fields)}
{_copy_button("Copy CNAME record", cname_record)}
</div>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
{_copy_row("Update-URL:", update_url, "Secret")} {_copy_row("Update-URL:", update_url, "Secret")}
{_copy_row("Domainnamen:", hostname, "Public")} {_copy_row("Domainnamen:", hostname, "Public")}
@@ -860,6 +868,31 @@ def _copy_row(label: str, value: str, sensitivity: str | None = None) -> str:
""" """
def _copy_button(label: str, value: str) -> str:
safe_label = html.escape(label)
safe_value = html.escape(value)
return f'<button type="button" class="btn btn-outline-primary rounded-pill" data-copy="{safe_value}">{safe_label}</button>'
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:
lines.append(f"Private status page: {management_url}")
return "\n".join(lines)
def _secret_notice() -> str: def _secret_notice() -> str:
return """ return """
<div class="alert alert-info my-3 py-2 small" role="note"> <div class="alert alert-info my-3 py-2 small" role="note">
@@ -893,6 +926,8 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
ipv4 = html.escape(str(account.get("ipv4") or "-")) ipv4 = html.escape(str(account.get("ipv4") or "-"))
ipv6 = html.escape(str(account.get("ipv6") or "-")) ipv6 = html.escape(str(account.get("ipv6") or "-"))
updated = html.escape(str(account.get("updated_at") or "Never")) updated = html.escape(str(account.get("updated_at") or "Never"))
fritzbox_fields = _fritzbox_fields(update_url, hostname, username, "unchanged; only shown when generated")
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
history_rows = "\n".join(_user_event_row(event) for event in service.store.list_update_events_for_hostname(str(account["hostname"]), 20)) or """ history_rows = "\n".join(_user_event_row(event) for event in service.store.list_update_events_for_hostname(str(account["hostname"]), 20)) or """
<tr><td colspan="5" class="empty">No router updates yet.</td></tr> <tr><td colspan="5" class="empty">No router updates yet.</td></tr>
""" """
@@ -917,6 +952,10 @@ def _render_management_page(service: DdnsService, settings: DdnsSettings, manage
<h2>FRITZ!Box fields</h2> <h2>FRITZ!Box fields</h2>
<p class="section-copy">Use these values in the FRITZ!Box custom DynDNS provider fields. If you use your own DNS name, point it at this hostname with a CNAME.</p> <p class="section-copy">Use these values in the FRITZ!Box custom DynDNS provider fields. If you use your own DNS name, point it at this hostname with a CNAME.</p>
{_secret_notice()} {_secret_notice()}
<div class="d-flex gap-2 flex-wrap mt-3">
{_copy_button("Copy FRITZ!Box fields", fritzbox_fields)}
{_copy_button("Copy CNAME record", cname_record)}
</div>
</div> </div>
<div class="col-lg-6 d-grid gap-2"> <div class="col-lg-6 d-grid gap-2">
{_copy_row("Update-URL:", update_url, "Secret")} {_copy_row("Update-URL:", update_url, "Secret")}
+6
View File
@@ -309,6 +309,8 @@ def test_self_service_managed_hostname_generates_random_account(tmp_path: Path)
assert response.status_code == 200 assert response.status_code == 200
assert "Update-URL:" in response.text assert "Update-URL:" in response.text
assert "CNAME target:" in response.text assert "CNAME target:" in response.text
assert "Copy FRITZ!Box fields" in response.text
assert "Copy CNAME record" in response.text
assert "Public:" in response.text assert "Public:" in response.text
assert "Secret:" in response.text assert "Secret:" in response.text
assert ".ddns.example.net" in response.text assert ".ddns.example.net" in response.text
@@ -333,6 +335,8 @@ def test_magic_hostname_without_login_generates_update_and_management_links(tmp_
assert "Update-URL:" in response.text assert "Update-URL:" in response.text
assert "CNAME target:" in response.text assert "CNAME target:" in response.text
assert "Private status page:" in response.text assert "Private status page:" in response.text
assert "Copy FRITZ!Box fields" in response.text
assert "Copy CNAME record" in response.text
assert response.text.count("Secret") >= 3 assert response.text.count("Secret") >= 3
assert response.text.count("Public") >= 3 assert response.text.count("Public") >= 3
assert ".ddns.example.net" in response.text assert ".ddns.example.net" in response.text
@@ -349,6 +353,8 @@ def test_magic_management_link_can_delete_hostname(tmp_path: Path) -> None:
assert management.status_code == 200 assert management.status_code == 200
assert "home.ddns.example.net" in management.text assert "home.ddns.example.net" in management.text
assert "CNAME target:" in management.text assert "CNAME target:" in management.text
assert "Copy FRITZ!Box fields" in management.text
assert "Copy CNAME record" in management.text
assert "Public:" in management.text assert "Public:" in management.text
assert "Secret:" in management.text assert "Secret:" in management.text
assert "Router updates" in management.text assert "Router updates" in management.text