2026-05-20 23:55:27 +02:00
from __future__ import annotations
from ..ddns_service import DdnsService
from .html_utils import SafeHtml , attr , escape , trusted
from .templates import (
_copy_button ,
_copy_row ,
_fritzbox_fields ,
_manual_update_commands ,
_manual_update_panel ,
_one_time_secret_notice ,
_secret_notice ,
)
def _linux_installer_section () -> SafeHtml :
system_install = "curl -fsSL https://karldns.de/install.sh | sudo sh"
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">
<div class="container narrow">
<div class="text-center mb-4">
<p class="eyebrow">Linux cron updater</p>
2026-05-21 10:05:40 +02:00
<h2>Linux updater.</h2>
<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>
2026-05-20 23:55:27 +02:00
</div>
<div class="card card-body gap-3">
<div class="terminal-card">
<code> { escape ( system_install ) } </code>
<code> { escape ( system_uninstall ) } </code>
</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 ) }
</div>
<div class="alert alert-info my-0 py-2 small" role="note">
2026-05-21 10:05:40 +02:00
Uninstall keeps your private URLs by default. Use <code>--delete-config</code> only when you want to remove everything.
2026-05-20 23:55:27 +02:00
</div>
<div>
2026-05-21 10:05:40 +02:00
<h3>User install</h3>
<p class="section-copy">Use this when you do not want a system-wide install.</p>
2026-05-20 23:55:27 +02:00
</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 )
2026-05-21 10:05:40 +02:00
status = escape ( message or "Add this TXT record at your DNS provider, then check again. DNS can take a few minutes." )
2026-05-20 23:55:27 +02:00
txt_bundle = f "TXT name: { verification_name } \n TXT 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>
2026-05-21 10:05:40 +02:00
<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 ) }
2026-05-20 23:55:27 +02:00
{ _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">
2026-05-21 10:05:40 +02:00
<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>
2026-05-20 23:55:27 +02:00
</div>
<div class="row g-3 mt-4 justify-content-center">
{ challenge_html }
{ create_form }
</div>
</div>
</section>
"""
)
def _credentials_panel ( service : DdnsService , created_account : dict [ str , str ] | None ) -> SafeHtml :
if not created_account :
return trusted ( "" )
hostname = created_account [ "hostname" ]
username = created_account [ "username" ]
password = created_account [ "password" ]
update_url = service . fritz_update_url ( created_account )
credentialed_update_url = service . credentialed_update_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 )
manual_commands = _manual_update_commands ( update_url )
return trusted (
f """
<section class="section success-section">
<div class="container">
2026-05-21 10:05:40 +02:00
<p class="eyebrow">Hostname ready</p>
2026-05-20 23:55:27 +02:00
<h2> { escape ( hostname ) } </h2>
2026-05-21 10:05:40 +02:00
<p class="section-copy">Use the Update-URL directly. Username and password are only for routers that require separate fields.</p>
2026-05-20 23:55:27 +02:00
{ _one_time_secret_notice () }
{ _secret_notice () }
<div class="d-flex gap-2 flex-wrap my-3">
2026-05-21 10:05:40 +02:00
{ _copy_button ( "Copy router fields" , fritzbox_fields ) }
{ _copy_button ( "Copy full URL" , credentialed_update_url ) }
{ _copy_button ( "Copy CNAME" , cname_record ) }
2026-05-20 23:55:27 +02:00
</div>
<div class="d-grid gap-2">
{ _copy_row ( "Update-URL:" , update_url , "Secret" ) }
2026-05-21 10:05:40 +02:00
{ _copy_row ( "Full update URL:" , credentialed_update_url , "Secret" ) }
2026-05-20 23:55:27 +02:00
{ _copy_row ( "Domainnamen:" , hostname , "Public" ) }
{ _copy_row ( "CNAME target:" , hostname , "Public" ) }
{ _copy_row ( "Benutzername (optional):" , username , "Public" ) }
{ _copy_row ( "Kennwort (optional):" , password , "Secret" ) }
2026-05-21 10:05:40 +02:00
{ _copy_row ( "Private dashboard:" , management_url , "Secret" ) }
2026-05-20 23:55:27 +02:00
</div>
{ _manual_update_panel ( manual_commands ) }
</div>
</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>
2026-05-21 10:05:40 +02:00
<h3>Create hostname</h3>
2026-05-20 23:55:27 +02:00
<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>
2026-05-21 10:05:40 +02:00
<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>
2026-05-20 23:55:27 +02:00
</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>
2026-05-21 10:05:40 +02:00
<h3>Create hostname</h3>
<p class="small text-secondary mb-0 mt-2">Available after KarlDNS can see the TXT record.</p>
2026-05-20 23:55:27 +02:00
</div>
</div>
"""
)