Archived
Refactor UI assets and harden quality checks
This commit is contained in:
1 parent
23ab466c52
commit
4ecc8eb874
62 files changed
+3314
-3206
No files matched your search
+223
-145
@@ -1,12 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..settings import DdnsSettings
|
||||
from .components import _code_window, _example_update_ips, _message_band
|
||||
from .html_utils import escape, trusted
|
||||
from .layout import _page, _top_nav
|
||||
from .html_utils import trusted
|
||||
from .layout import PageOptions, _page, _top_nav
|
||||
from .sections.public_home import _manual_updates_section, _public_home_hero, _setup_flow_section
|
||||
from .templates_public_sections import _linux_installer_section
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _ApiDocsExamples:
|
||||
create: str
|
||||
register: str
|
||||
update: str
|
||||
status: str
|
||||
rotate: str
|
||||
schema: str
|
||||
|
||||
|
||||
def _render_public_home(
|
||||
settings: DdnsSettings,
|
||||
message: str | None,
|
||||
@@ -14,8 +27,7 @@ def _render_public_home(
|
||||
) -> str:
|
||||
base_url = settings.public_base_url.rstrip("/")
|
||||
hostname_suffix = settings.hostname_suffix or "your KarlDNS suffix"
|
||||
suffix = escape(hostname_suffix)
|
||||
demo_update_url = escape(f"{base_url}/api/v1/router/update/<secret>?myip=<ipaddr>")
|
||||
demo_update_url = f"{base_url}/api/v1/router/update/<secret>?myip=<ipaddr>"
|
||||
example_ipv4, _ = _example_update_ips(request_ip)
|
||||
create_demo_command = f"curl -sS '{base_url}/api/v1/router/update/<secret>?myip={example_ipv4}'"
|
||||
create_demo = f"{create_demo_command}\ngood {example_ipv4}"
|
||||
@@ -28,81 +40,55 @@ def _render_public_home(
|
||||
]
|
||||
)
|
||||
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."
|
||||
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"""
|
||||
<main id="main">
|
||||
{_top_nav()}
|
||||
<section class="hero-band">
|
||||
<div class="container text-center d-grid justify-items-center gap-3">
|
||||
<img class="hero-logo" src="/logo.webp?v=3" alt="" width="80" height="80" decoding="async" fetchpriority="high">
|
||||
<h1>Dynamic DNS for home routers.</h1>
|
||||
<div class="install-pill mx-auto">
|
||||
<code>{demo_update_url}</code>
|
||||
</div>
|
||||
<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">
|
||||
<form method="post" action="/" class="hero-action-form" aria-label="Create a generated DynDNS hostname">
|
||||
<button type="submit" class="btn btn-primary rounded-pill">Create hostname</button>
|
||||
</form>
|
||||
<a class="btn btn-outline-primary rounded-pill" href="#manual-updates">Manual updates</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{_public_home_hero(suffix=hostname_suffix, demo_update_url=demo_update_url)}
|
||||
{message_html}
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow text-center">
|
||||
<p class="eyebrow">How it works</p>
|
||||
<h2>No account needed.</h2>
|
||||
<p class="section-copy mx-auto">Create a hostname, save the private values, register it, done.</p>
|
||||
<ol class="flow-steps" aria-label="KarlDNS setup flow">
|
||||
<li><strong>Create</strong><span>Random hostname and private links.</span></li>
|
||||
<li><strong>Save</strong><span>Copy the secrets while they are shown.</span></li>
|
||||
<li><strong>Register</strong><span>Activate the hostname when you are ready.</span></li>
|
||||
<li><strong>Update</strong><span>Router, cron, or CNAME can use it.</span></li>
|
||||
</ol>
|
||||
{_code_window(create_demo, copy_label="update example", class_name="mt-4", copy_value=create_demo_command)}
|
||||
</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>
|
||||
{_code_window(manual_demo, copy_label="manual update examples", class_name="manual-code text-start mt-4")}
|
||||
</div>
|
||||
</section>
|
||||
{_setup_flow_section(create_demo=create_demo, create_demo_command=create_demo_command)}
|
||||
{_manual_updates_section(manual_demo=manual_demo)}
|
||||
{_linux_installer_section(base_url)}
|
||||
</main>
|
||||
"""),
|
||||
description=description,
|
||||
preload_logo=True,
|
||||
canonical_url=f"{base_url}/",
|
||||
image_url=f"{base_url}/social-preview.png",
|
||||
image_alt="KarlDNS landing page with private Dynamic DNS update URL",
|
||||
structured_data={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "KarlDNS",
|
||||
"applicationCategory": "NetworkingApplication",
|
||||
"operatingSystem": "Linux",
|
||||
"url": f"{base_url}/",
|
||||
"description": description,
|
||||
"image": f"{base_url}/social-preview.png",
|
||||
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "EUR"},
|
||||
"featureList": [
|
||||
"Dynamic DNS for FRITZ!Box routers",
|
||||
"Accountless private update URLs",
|
||||
"CNAME target for external DNS names",
|
||||
"Linux cron updater",
|
||||
],
|
||||
},
|
||||
PageOptions(
|
||||
description=description,
|
||||
preload_logo=True,
|
||||
canonical_url=f"{base_url}/",
|
||||
image_url=f"{base_url}/social-preview.png",
|
||||
image_alt="KarlDNS landing page with private Dynamic DNS update URL",
|
||||
structured_data={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "KarlDNS",
|
||||
"applicationCategory": "NetworkingApplication",
|
||||
"operatingSystem": "Linux",
|
||||
"url": f"{base_url}/",
|
||||
"description": description,
|
||||
"image": f"{base_url}/social-preview.png",
|
||||
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "EUR"},
|
||||
"featureList": [
|
||||
"Dynamic DNS for FRITZ!Box routers",
|
||||
"Accountless private update URLs",
|
||||
"CNAME target for external DNS names",
|
||||
"Linux cron updater",
|
||||
],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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, 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"""
|
||||
@@ -119,113 +105,205 @@ 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, protected dashboard tokens, private update links, recent IP history, timestamps, and service events.</p>
|
||||
<p class="section-copy">
|
||||
KarlDNS stores hostnames, protected dashboard tokens, private update links,
|
||||
recent IP history, timestamps, and service events.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Secrets</h2>
|
||||
<p class="section-copy">Dashboard links, registration URLs, and update URLs are secrets. Anyone with them may update or manage the hostname. Dashboard lookup values are not stored as readable text.</p>
|
||||
<p class="section-copy">
|
||||
Dashboard links, registration URLs, and update URLs are secrets.
|
||||
Anyone with them may update or manage the hostname.
|
||||
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 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>
|
||||
<p class="section-copy">For the public KarlDNS instance, contact Karl through <a href="https://karl.fail" rel="noopener">karl.fail</a> or <a href="https://karlcom.de" rel="noopener">karlcom.de</a>.</p>
|
||||
<p class="section-copy">
|
||||
For the public KarlDNS instance, contact Karl through
|
||||
<a href="https://karl.fail" rel="noopener">karl.fail</a> or
|
||||
<a href="https://karlcom.de" rel="noopener">karlcom.de</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
"""),
|
||||
description=description,
|
||||
canonical_url=f"{base_url}/privacy",
|
||||
image_url=f"{base_url}/social-preview.png",
|
||||
image_alt="KarlDNS legal and privacy information",
|
||||
structured_data={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "KarlDNS Legal and Privacy",
|
||||
"url": f"{base_url}/privacy",
|
||||
"description": description,
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "KarlDNS",
|
||||
"url": f"{base_url}/",
|
||||
PageOptions(
|
||||
description=description,
|
||||
canonical_url=f"{base_url}/privacy",
|
||||
image_url=f"{base_url}/social-preview.png",
|
||||
image_alt="KarlDNS legal and privacy information",
|
||||
structured_data={
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebPage",
|
||||
"name": "KarlDNS Legal and Privacy",
|
||||
"url": f"{base_url}/privacy",
|
||||
"description": description,
|
||||
"isPartOf": {
|
||||
"@type": "WebSite",
|
||||
"name": "KarlDNS",
|
||||
"url": f"{base_url}/",
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None) -> str:
|
||||
base_url = settings.public_base_url.rstrip("/")
|
||||
example_ipv4, _ = _example_update_ips(request_ip)
|
||||
create_example = f"curl -sS -X POST {base_url}/api/v1/hostnames"
|
||||
register_example = (
|
||||
f"curl -sS -X POST '{base_url}/api/v1/router/register/<karl_dashboard_secret>?myip={example_ipv4}'"
|
||||
examples = _api_docs_examples(base_url, example_ipv4)
|
||||
description = (
|
||||
"Small KarlDNS API guide for creating, activating, updating, reading, and deleting "
|
||||
"generated Dynamic DNS hostnames."
|
||||
)
|
||||
update_example = f"curl -sS '{base_url}/api/v1/router/update/<karl_update_secret>?myip={example_ipv4}'"
|
||||
status_example = f"curl -sS '{base_url}/api/v1/hostnames/<karl_dashboard_secret>?history_limit=10'"
|
||||
rotate_example = f"curl -sS -X POST {base_url}/api/v1/hostnames/<karl_dashboard_secret>/rotate-update-url"
|
||||
schema_example = f"curl -sS {base_url}/openapi.json"
|
||||
description = "Small KarlDNS API guide for creating, activating, updating, reading, and deleting generated Dynamic DNS hostnames."
|
||||
return _page(
|
||||
"KarlDNS API",
|
||||
trusted(f"""
|
||||
<main id="main">
|
||||
{_top_nav()}
|
||||
<section class="hero-band compact">
|
||||
<div class="container text-center d-grid justify-items-center gap-3">
|
||||
<p class="eyebrow">API</p>
|
||||
<h1>Small API surface.</h1>
|
||||
<p class="lead">Create a hostname, register it, update it, and read its status.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
<div class="api-endpoints">
|
||||
<h2>Endpoints</h2>
|
||||
<ol class="simple-steps text-start">
|
||||
<li><strong>POST /api/v1/hostnames.</strong> Create a pending hostname and private setup values. The response includes <code>registration_url</code>.</li>
|
||||
<li><strong>GET or POST /api/v1/router/register/<karl_secret>.</strong> Register the hostname after secrets are saved. Publishes an initial DNS record from a public request IP or explicit <code>myip</code>/<code>myipv6</code> value. If no public IP is visible, publishes a temporary <code>192.0.2.1</code> A record until the first router update.</li>
|
||||
<li><strong>GET or POST /api/v1/router/update/<karl_secret>.</strong> Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>, <code>myipv6</code>, and <code>ip6addr</code>.</li>
|
||||
<li><strong>GET /api/v1/hostnames/<karl_secret>.</strong> Read hostname status and recent history.</li>
|
||||
<li><strong>POST /api/v1/hostnames/<karl_secret>/rotate-update-url.</strong> Replace the private router update URL.</li>
|
||||
<li><strong>DELETE /api/v1/hostnames/<karl_secret>.</strong> Delete hostname and DNS records.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Create</h2>
|
||||
{_code_window(create_example, copy_label="create command", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Register</h2>
|
||||
{_code_window(register_example, copy_label="register command", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Update</h2>
|
||||
{_code_window(update_example, copy_label="update command", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Status</h2>
|
||||
<p class="section-copy">Use the private dashboard secret to read current DNS state and recent updates for only that hostname.</p>
|
||||
{_code_window(status_example, copy_label="status command", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Rotate update URL</h2>
|
||||
<p class="section-copy">Use the private dashboard secret to replace the router update URL. The old update URL stops working immediately.</p>
|
||||
{_code_window(rotate_example, copy_label="rotate command", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Schema</h2>
|
||||
<p class="section-copy">Machine-readable OpenAPI is available for the public hostname and router endpoints.</p>
|
||||
{_code_window(schema_example, copy_label="schema command", class_name="mt-3")}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{_api_docs_hero()}
|
||||
{_api_docs_content(examples)}
|
||||
</main>
|
||||
"""),
|
||||
description=description,
|
||||
canonical_url=f"{base_url}/docs",
|
||||
image_url=f"{base_url}/social-preview.png",
|
||||
image_alt="KarlDNS API documentation",
|
||||
PageOptions(
|
||||
description=description,
|
||||
canonical_url=f"{base_url}/docs",
|
||||
image_url=f"{base_url}/social-preview.png",
|
||||
image_alt="KarlDNS API documentation",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _api_docs_examples(base_url: str, example_ipv4: str) -> _ApiDocsExamples:
|
||||
return _ApiDocsExamples(
|
||||
create=f"curl -sS -X POST {base_url}/api/v1/hostnames",
|
||||
register=f"curl -sS -X POST '{base_url}/api/v1/router/register/<karl_dashboard_secret>?myip={example_ipv4}'",
|
||||
update=f"curl -sS '{base_url}/api/v1/router/update/<karl_update_secret>?myip={example_ipv4}'",
|
||||
status=f"curl -sS '{base_url}/api/v1/hostnames/<karl_dashboard_secret>?history_limit=10'",
|
||||
rotate=f"curl -sS -X POST {base_url}/api/v1/hostnames/<karl_dashboard_secret>/rotate-update-url",
|
||||
schema=f"curl -sS {base_url}/openapi.json",
|
||||
)
|
||||
|
||||
|
||||
def _api_docs_hero() -> str:
|
||||
return """
|
||||
<section class="hero-band compact">
|
||||
<div class="container text-center d-grid justify-items-center gap-3">
|
||||
<p class="eyebrow">API</p>
|
||||
<h1>Small API surface.</h1>
|
||||
<p class="lead">Create a hostname, register it, update it, and read its status.</p>
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
|
||||
|
||||
def _api_docs_content(examples: _ApiDocsExamples) -> str:
|
||||
return f"""
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
{_api_endpoints_overview()}
|
||||
{_api_example_block("Create", examples.create, "create command")}
|
||||
{_api_example_block("Register", examples.register, "register command")}
|
||||
{_api_example_block("Update", examples.update, "update command")}
|
||||
{_api_status_block(examples.status)}
|
||||
{_api_rotate_block(examples.rotate)}
|
||||
{_api_schema_block(examples.schema)}
|
||||
</div>
|
||||
</section>
|
||||
"""
|
||||
|
||||
|
||||
def _api_endpoints_overview() -> str:
|
||||
return """
|
||||
<div class="api-endpoints">
|
||||
<h2>Endpoints</h2>
|
||||
<ol class="simple-steps text-start">
|
||||
<li>
|
||||
<strong>POST /api/v1/hostnames.</strong>
|
||||
Create a pending hostname and private setup values. The response includes
|
||||
<code>registration_url</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>GET or POST /api/v1/router/register/<karl_secret>.</strong>
|
||||
Register the hostname after secrets are saved. Publishes an initial DNS record
|
||||
from a public request IP or explicit <code>myip</code>/<code>myipv6</code> value.
|
||||
If no public IP is visible, publishes a temporary <code>192.0.2.1</code> A record
|
||||
until the first router update.
|
||||
</li>
|
||||
<li>
|
||||
<strong>GET or POST /api/v1/router/update/<karl_secret>.</strong>
|
||||
Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>,
|
||||
<code>myipv6</code>, and <code>ip6addr</code>.
|
||||
</li>
|
||||
<li>
|
||||
<strong>GET /api/v1/hostnames/<karl_secret>.</strong>
|
||||
Read hostname status and recent history.
|
||||
</li>
|
||||
<li>
|
||||
<strong>POST /api/v1/hostnames/<karl_secret>/rotate-update-url.</strong>
|
||||
Replace the private router update URL.
|
||||
</li>
|
||||
<li>
|
||||
<strong>DELETE /api/v1/hostnames/<karl_secret>.</strong>
|
||||
Delete hostname and DNS records.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _api_example_block(title: str, command: str, copy_label: str) -> str:
|
||||
return f"""
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{_code_window(command, copy_label=copy_label, class_name="mt-3")}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _api_status_block(command: str) -> str:
|
||||
return f"""
|
||||
<div>
|
||||
<h2>Status</h2>
|
||||
<p class="section-copy">
|
||||
Use the private dashboard secret to read current DNS state and recent updates
|
||||
for only that hostname.
|
||||
</p>
|
||||
{_code_window(command, copy_label="status command", class_name="mt-3")}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _api_rotate_block(command: str) -> str:
|
||||
return f"""
|
||||
<div>
|
||||
<h2>Rotate update URL</h2>
|
||||
<p class="section-copy">
|
||||
Use the private dashboard secret to replace the router update URL.
|
||||
The old update URL stops working immediately.
|
||||
</p>
|
||||
{_code_window(command, copy_label="rotate command", class_name="mt-3")}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _api_schema_block(command: str) -> str:
|
||||
return f"""
|
||||
<div>
|
||||
<h2>Schema</h2>
|
||||
<p class="section-copy">
|
||||
Machine-readable OpenAPI is available for the public hostname and router endpoints.
|
||||
</p>
|
||||
{_code_window(command, copy_label="schema command", class_name="mt-3")}
|
||||
</div>
|
||||
"""
|
||||
Reference in new issue
Block a user