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
+402
-209
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..services.ddns import DdnsService
|
||||
from ..settings import DdnsSettings
|
||||
from .components import (
|
||||
_code_window,
|
||||
_copy_button,
|
||||
@@ -14,229 +15,383 @@ from .components import (
|
||||
_secret_notice,
|
||||
)
|
||||
from .html_utils import SafeHtml, attr, escape, trusted
|
||||
from .layout import _page, _top_nav
|
||||
from .layout import PageOptions, _page, _top_nav
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..settings import DdnsSettings
|
||||
|
||||
|
||||
def _render_management_page(
|
||||
service: DdnsService,
|
||||
settings: DdnsSettings,
|
||||
management_slug: str,
|
||||
account: Mapping[str, object],
|
||||
csrf_token: str,
|
||||
message: str | None = None,
|
||||
request_ip: str | None = None,
|
||||
) -> str:
|
||||
account_for_url = {
|
||||
"hostname": str(account["hostname"]),
|
||||
"update_slug": str(account["update_slug"]),
|
||||
"management_slug": management_slug,
|
||||
}
|
||||
update_url = service.fritz_update_url(account_for_url)
|
||||
hostname = escape(account["hostname"])
|
||||
ipv4 = escape(account.get("ipv4") or "-")
|
||||
ipv6 = escape(account.get("ipv6") or "-")
|
||||
updated = escape(account.get("updated_at") or "Never")
|
||||
fritzbox_fields = _fritzbox_fields(update_url, hostname)
|
||||
cname_record = f"your-subdomain.example.com. CNAME {hostname}."
|
||||
manual_commands = _manual_update_commands(update_url, request_ip)
|
||||
history_rows = (
|
||||
"\n".join(
|
||||
_user_event_row(event)
|
||||
for event in service.store.list_update_events_for_hostname(str(account["hostname"]), 10)
|
||||
)
|
||||
or """
|
||||
<tr><td colspan="6" class="empty">No router updates yet.</td></tr>
|
||||
"""
|
||||
@dataclass(frozen=True)
|
||||
class ManagementPageContext:
|
||||
management_slug: str
|
||||
account: Mapping[str, object]
|
||||
csrf_token: str
|
||||
message: str | None = None
|
||||
request_ip: str | None = None
|
||||
|
||||
@property
|
||||
def account_for_url(self) -> dict[str, str]:
|
||||
return {
|
||||
"hostname": str(self.account["hostname"]),
|
||||
"update_slug": str(self.account["update_slug"]),
|
||||
"management_slug": self.management_slug,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _ManagementPageView:
|
||||
update_url: str
|
||||
hostname: str
|
||||
ipv4: str
|
||||
ipv6: str
|
||||
updated: str
|
||||
fritzbox_fields: str
|
||||
cname_record: str
|
||||
manual_commands: str
|
||||
|
||||
|
||||
def _management_page_view(update_url: str, context: ManagementPageContext) -> _ManagementPageView:
|
||||
hostname = escape(context.account["hostname"])
|
||||
return _ManagementPageView(
|
||||
update_url=update_url,
|
||||
hostname=hostname,
|
||||
ipv4=escape(context.account.get("ipv4") or "-"),
|
||||
ipv6=escape(context.account.get("ipv6") or "-"),
|
||||
updated=escape(context.account.get("updated_at") or "Never"),
|
||||
fritzbox_fields=_fritzbox_fields(update_url, hostname),
|
||||
cname_record=f"your-subdomain.example.com. CNAME {hostname}.",
|
||||
manual_commands=_manual_update_commands(update_url, context.request_ip),
|
||||
)
|
||||
message_html = _management_message(message)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PendingManagementPageContext:
|
||||
settings: DdnsSettings
|
||||
management_slug: str
|
||||
account: Mapping[str, object]
|
||||
csrf_token: str
|
||||
message: str | None = None
|
||||
request_ip: str | None = None
|
||||
|
||||
@property
|
||||
def account_for_url(self) -> dict[str, str]:
|
||||
return {
|
||||
"hostname": str(self.account["hostname"]),
|
||||
"update_slug": str(self.account["update_slug"]),
|
||||
"management_slug": self.management_slug,
|
||||
}
|
||||
|
||||
|
||||
def _render_management_page(service: DdnsService, context: ManagementPageContext) -> str:
|
||||
update_url = service.fritz_update_url(context.account_for_url)
|
||||
view = _management_page_view(update_url, context)
|
||||
message_html = _management_message(context.message)
|
||||
dashboard_password_panel = _dashboard_password_panel(
|
||||
management_slug, csrf_token, bool(account.get("dashboard_password_hash"))
|
||||
context.management_slug,
|
||||
context.csrf_token,
|
||||
bool(context.account.get("dashboard_password_hash")),
|
||||
)
|
||||
dashboard_shell = _management_dashboard_shell(context, view, message_html)
|
||||
history_section = _management_history_section(_management_history_rows(service, context))
|
||||
delete_section = _management_delete_section(context)
|
||||
return _page(
|
||||
"KarlDNS management",
|
||||
trusted(f"""
|
||||
<main id="main" class="dashboard-page">
|
||||
{_top_nav()}
|
||||
<section class="dashboard-shell">
|
||||
<div class="container">
|
||||
<div class="dashboard-header">
|
||||
<div>
|
||||
<p class="eyebrow">Dashboard</p>
|
||||
<h1>{hostname}</h1>
|
||||
<p class="lead">Private management link. Keep it safe.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-primary rounded-pill" href="/">Home</a>
|
||||
</div>
|
||||
{message_html}
|
||||
<div class="dashboard-grid">
|
||||
<section class="dashboard-card dashboard-card-main" aria-labelledby="router-settings-title">
|
||||
<div class="dashboard-card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Router settings</p>
|
||||
<h2 id="router-settings-title">Update endpoint</h2>
|
||||
</div>
|
||||
<form method="post" action="/m/{attr(management_slug)}/rotate">
|
||||
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<button class="btn btn-outline-primary rounded-pill" type="submit">New update URL</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="section-copy">Paste the Update-URL into your router. Use the hostname as Domainnamen, or point your own subdomain at it with a CNAME.</p>
|
||||
{_secret_notice()}
|
||||
<div class="dashboard-copy-stack">
|
||||
{_copy_row("Update-URL:", update_url, "Secret")}
|
||||
{_copy_row("Domainnamen:", hostname, "Public")}
|
||||
{_copy_row("CNAME target:", hostname, "Public")}
|
||||
</div>
|
||||
<div class="dashboard-actions">
|
||||
{_copy_button("Router fields", fritzbox_fields)}
|
||||
{_copy_button("Copy CNAME", cname_record, style="outline-secondary")}
|
||||
</div>
|
||||
</section>
|
||||
<section class="dashboard-card dashboard-status-card" aria-labelledby="status-title">
|
||||
<p class="eyebrow">Current address</p>
|
||||
<h2 id="status-title">Status</h2>
|
||||
<dl class="dashboard-status-list">
|
||||
<div><dt>IPv4</dt><dd>{ipv4}</dd></div>
|
||||
<div><dt>IPv6</dt><dd>{ipv6}</dd></div>
|
||||
<div><dt>Updated</dt><dd>{updated}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
<section class="dashboard-card dashboard-card-compact" aria-labelledby="manual-updates-title">
|
||||
<div class="dashboard-card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Manual updates</p>
|
||||
<h2 id="manual-updates-title">curl examples</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="section-copy">FRITZ!Box fills IP placeholders automatically. Scripts can send an IP explicitly or omit it and let KarlDNS use the request IP.</p>
|
||||
{_code_window(manual_commands, copy_label="manual update commands", class_name="manual-code")}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
{dashboard_shell}
|
||||
{dashboard_password_panel}
|
||||
<section class="dashboard-section">
|
||||
<div class="container">
|
||||
<div class="dashboard-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">History</p>
|
||||
<h2>Router updates</h2>
|
||||
<p class="section-copy">Recent IP changes and failed updates for this hostname.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive dashboard-table-shell">
|
||||
<table class="table table-hover dashboard-history-table mb-0">
|
||||
<thead><tr><th>Time</th><th>Status</th><th>IPv4</th><th>IPv6</th><th>Count</th><th>Detail</th></tr></thead>
|
||||
<tbody>{history_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="dashboard-section dashboard-danger-section">
|
||||
<div class="container">
|
||||
<div class="dashboard-danger-card">
|
||||
<div>
|
||||
<p class="eyebrow">Delete</p>
|
||||
<h2>Delete hostname</h2>
|
||||
<p class="section-copy">Remove this hostname and its DNS records. The update URL will stop working.</p>
|
||||
</div>
|
||||
<form method="post" action="/m/{attr(management_slug)}/delete">
|
||||
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
|
||||
<button class="btn btn-outline-danger rounded-pill" type="submit">Delete hostname</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{history_section}
|
||||
{delete_section}
|
||||
</main>
|
||||
"""),
|
||||
extra_styles=trusted('<link href="/admin.css?v=5" rel="stylesheet">'),
|
||||
PageOptions(extra_styles=trusted('<link href="/admin.css?v=5" rel="stylesheet">')),
|
||||
)
|
||||
|
||||
|
||||
def _render_pending_management_page(
|
||||
service: DdnsService,
|
||||
settings: DdnsSettings,
|
||||
management_slug: str,
|
||||
account: Mapping[str, object],
|
||||
csrf_token: str,
|
||||
message: str | None = None,
|
||||
request_ip: str | None = None,
|
||||
) -> str:
|
||||
raw_hostname = str(account["hostname"])
|
||||
account_for_url = {
|
||||
"hostname": raw_hostname,
|
||||
"update_slug": str(account["update_slug"]),
|
||||
"management_slug": management_slug,
|
||||
}
|
||||
update_url = service.fritz_update_url(account_for_url)
|
||||
dashboard_url = service.dashboard_url({"management_slug": management_slug})
|
||||
def _management_history_rows(service: DdnsService, context: ManagementPageContext) -> SafeHtml:
|
||||
events = service.store.list_update_events_for_hostname(str(context.account["hostname"]), 10)
|
||||
rows = "\n".join(_user_event_row(event) for event in events)
|
||||
return trusted(rows or """
|
||||
<tr class="empty-row">
|
||||
<td colspan="6" class="empty">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-title">No router updates yet.</div>
|
||||
<div class="empty-state-description">
|
||||
This table fills after the router or a script sends its first address update.
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
""")
|
||||
|
||||
|
||||
def _management_dashboard_shell(
|
||||
context: ManagementPageContext,
|
||||
view: _ManagementPageView,
|
||||
message_html: SafeHtml,
|
||||
) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="dashboard-shell">
|
||||
<div class="container">
|
||||
{_management_dashboard_header(view)}
|
||||
{message_html}
|
||||
<div class="dashboard-grid">
|
||||
{_router_settings_card(context, view)}
|
||||
{_status_card(view)}
|
||||
</div>
|
||||
{_manual_updates_card(view)}
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _management_dashboard_header(view: _ManagementPageView) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<div class="dashboard-header">
|
||||
<div>
|
||||
<p class="eyebrow">Dashboard</p>
|
||||
<h1>{view.hostname}</h1>
|
||||
<p class="lead">Private management link. Keep it safe.</p>
|
||||
</div>
|
||||
<a class="btn btn-outline-primary rounded-pill" href="/">Home</a>
|
||||
</div>
|
||||
""")
|
||||
|
||||
|
||||
def _router_settings_card(context: ManagementPageContext, view: _ManagementPageView) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="dashboard-card dashboard-card-main" data-slot="card" aria-labelledby="router-settings-title">
|
||||
<div class="dashboard-card-header" data-slot="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Router settings</p>
|
||||
<h2 id="router-settings-title">Update endpoint</h2>
|
||||
</div>
|
||||
<form method="post" action="/m/{attr(context.management_slug)}/rotate">
|
||||
<input type="hidden" name="csrf" value="{attr(context.csrf_token)}">
|
||||
<input type="hidden" name="action" value="update">
|
||||
<button class="btn btn-outline-primary rounded-pill" type="submit">New update URL</button>
|
||||
</form>
|
||||
</div>
|
||||
<div data-slot="card-content">
|
||||
<p class="section-copy">
|
||||
Paste the Update-URL into your router. Use the hostname as Domainnamen,
|
||||
or point your own subdomain at it with a CNAME.
|
||||
</p>
|
||||
{_secret_notice()}
|
||||
<div class="dashboard-copy-stack">
|
||||
{_copy_row("Update-URL:", view.update_url, "Secret")}
|
||||
{_copy_row("Domainnamen:", view.hostname, "Public")}
|
||||
{_copy_row("CNAME target:", view.hostname, "Public")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-actions" data-slot="card-footer">
|
||||
{_copy_button("Router fields", view.fritzbox_fields)}
|
||||
{_copy_button("Copy CNAME", view.cname_record, style="outline-secondary")}
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _status_card(view: _ManagementPageView) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="dashboard-card dashboard-status-card" data-slot="card" aria-labelledby="status-title">
|
||||
<div data-slot="card-header">
|
||||
<p class="eyebrow">Current address</p>
|
||||
<h2 id="status-title">Status</h2>
|
||||
</div>
|
||||
<div data-slot="card-content">
|
||||
<dl class="dashboard-status-list">
|
||||
<div><dt>IPv4</dt><dd>{view.ipv4}</dd></div>
|
||||
<div><dt>IPv6</dt><dd>{view.ipv6}</dd></div>
|
||||
<div><dt>Updated</dt><dd>{view.updated}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _manual_updates_card(view: _ManagementPageView) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="dashboard-card dashboard-card-compact" data-slot="card" aria-labelledby="manual-updates-title">
|
||||
<div class="dashboard-card-header" data-slot="card-header">
|
||||
<div>
|
||||
<p class="eyebrow">Manual updates</p>
|
||||
<h2 id="manual-updates-title">curl examples</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="card-content">
|
||||
<p class="section-copy">
|
||||
FRITZ!Box fills IP placeholders automatically. Scripts can send an IP explicitly
|
||||
or omit it and let KarlDNS use the request IP.
|
||||
</p>
|
||||
{_code_window(view.manual_commands, copy_label="manual update commands", class_name="manual-code")}
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _management_history_section(history_rows: SafeHtml) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="dashboard-section">
|
||||
<div class="container">
|
||||
<div class="dashboard-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">History</p>
|
||||
<h2>Router updates</h2>
|
||||
<p class="section-copy">Recent IP changes and failed updates for this hostname.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive dashboard-table-shell">
|
||||
<table class="table table-hover dashboard-history-table mb-0">
|
||||
<thead><tr><th>Time</th><th>Status</th><th>IPv4</th><th>IPv6</th><th>Count</th><th>Detail</th></tr></thead>
|
||||
<tbody>{history_rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _management_delete_section(context: ManagementPageContext) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="dashboard-section dashboard-danger-section">
|
||||
<div class="container">
|
||||
<div class="dashboard-danger-card" data-slot="card">
|
||||
<div>
|
||||
<p class="eyebrow">Delete</p>
|
||||
<h2>Delete hostname</h2>
|
||||
<p class="section-copy">
|
||||
Remove this hostname and its DNS records. The update URL will stop working.
|
||||
</p>
|
||||
</div>
|
||||
<form method="post" action="/m/{attr(context.management_slug)}/delete">
|
||||
<input type="hidden" name="csrf" value="{attr(context.csrf_token)}">
|
||||
<button class="btn btn-outline-danger rounded-pill" type="submit">Delete hostname</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _render_pending_management_page(service: DdnsService, context: PendingManagementPageContext) -> str:
|
||||
raw_hostname = str(context.account["hostname"])
|
||||
update_url = service.fritz_update_url(context.account_for_url)
|
||||
dashboard_url = service.dashboard_url({"management_slug": context.management_slug})
|
||||
hostname = escape(raw_hostname)
|
||||
message_html = _management_message(message)
|
||||
cleanup_hours = settings.cleanup_pending_account_hours
|
||||
message_html = _management_message(context.message)
|
||||
cleanup_hours = context.settings.cleanup_pending_account_hours
|
||||
cleanup_text = f"{cleanup_hours} hours" if cleanup_hours != 24 else "24 hours"
|
||||
manual_commands = _manual_update_commands(update_url, request_ip)
|
||||
pending_lead = (
|
||||
"Save these values, then register the hostname. Pending setups are cleaned up after " f"{cleanup_text}."
|
||||
)
|
||||
manual_commands = _manual_update_commands(update_url, context.request_ip)
|
||||
hero = _pending_hero(hostname, pending_lead)
|
||||
values_section = _pending_values_section(context, update_url, raw_hostname, dashboard_url)
|
||||
router_section = _pending_router_section(hostname, update_url, manual_commands)
|
||||
return _page(
|
||||
"KarlDNS setup",
|
||||
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">Pending setup</p>
|
||||
<h1>{hostname}</h1>
|
||||
<p class="lead">Save these values, then register the hostname. Pending setups are cleaned up after {escape(cleanup_text)}.</p>
|
||||
</div>
|
||||
</section>
|
||||
{hero}
|
||||
{message_html}
|
||||
<section class="section">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
<div>
|
||||
<p class="eyebrow">Save</p>
|
||||
<h2>Your values.</h2>
|
||||
<p class="section-copy">Save the update URL and dashboard link before you register. After registration, the dashboard hides secrets by default.</p>
|
||||
<p class="section-copy"><strong>Public:</strong> Domainnamen. <strong>Secret:</strong> Update-URL and private dashboard.</p>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
{_copy_row("Update-URL:", update_url, "Secret", mask_secret=False)}
|
||||
{_copy_row("Domainnamen:", raw_hostname, "Public")}
|
||||
{_copy_row("Private dashboard:", dashboard_url, "Secret", mask_secret=False)}
|
||||
</div>
|
||||
<form method="post" action="/m/{attr(management_slug)}/activate">
|
||||
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
|
||||
<button class="btn btn-primary rounded-pill" type="submit">Register hostname</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
<div>
|
||||
<p class="eyebrow">FRITZ!Box</p>
|
||||
<h2>Router setup.</h2>
|
||||
<p class="section-copy">Paste this exact URL into the FRITZ!Box <strong>Update-URL</strong> field. Use <strong>{hostname}</strong> as <strong>Domainnamen</strong>. No KarlDNS username or password is needed; if your router requires them, enter any placeholder.</p>
|
||||
{_code_window(update_url, copy_label="FRITZ!Box update URL", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Manual updates.</h2>
|
||||
<p class="section-copy">Scripts can send the IP explicitly, or omit it and let KarlDNS use the request IP. Visible examples use your public IP when KarlDNS or your browser can detect it.</p>
|
||||
{_code_window(manual_commands, copy_label="manual update commands", class_name="manual-code mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use your own DNS name.</h2>
|
||||
<p class="section-copy">Create a CNAME at your DNS provider that points to <strong>{hostname}</strong>. This is the same value as Domainnamen.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{values_section}
|
||||
{router_section}
|
||||
</main>
|
||||
"""),
|
||||
)
|
||||
|
||||
|
||||
def _pending_hero(hostname: str, pending_lead: str) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="hero-band compact">
|
||||
<div class="container text-center d-grid justify-items-center gap-3">
|
||||
<p class="eyebrow">Pending setup</p>
|
||||
<h1>{hostname}</h1>
|
||||
<p class="lead">{escape(pending_lead)}</p>
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _pending_values_section(
|
||||
context: PendingManagementPageContext,
|
||||
update_url: str,
|
||||
raw_hostname: str,
|
||||
dashboard_url: str,
|
||||
) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="section">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
<div>
|
||||
<p class="eyebrow">Save</p>
|
||||
<h2>Your values.</h2>
|
||||
<p class="section-copy">
|
||||
Save the update URL and dashboard link before you register. After registration,
|
||||
the dashboard hides secrets by default.
|
||||
</p>
|
||||
<p class="section-copy">
|
||||
<strong>Public:</strong> Domainnamen.
|
||||
<strong>Secret:</strong> Update-URL and private dashboard.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
{_copy_row("Update-URL:", update_url, "Secret", mask_secret=False)}
|
||||
{_copy_row("Domainnamen:", raw_hostname, "Public")}
|
||||
{_copy_row("Private dashboard:", dashboard_url, "Secret", mask_secret=False)}
|
||||
</div>
|
||||
<form method="post" action="/m/{attr(context.management_slug)}/activate">
|
||||
<input type="hidden" name="csrf" value="{attr(context.csrf_token)}">
|
||||
<button class="btn btn-primary rounded-pill" type="submit">Register hostname</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _pending_router_section(hostname: str, update_url: str, manual_commands: str) -> SafeHtml:
|
||||
return trusted(f"""
|
||||
<section class="section section-compact">
|
||||
<div class="container narrow d-grid gap-4">
|
||||
<div>
|
||||
<p class="eyebrow">FRITZ!Box</p>
|
||||
<h2>Router setup.</h2>
|
||||
<p class="section-copy">
|
||||
Paste this exact URL into the FRITZ!Box <strong>Update-URL</strong> field.
|
||||
Use <strong>{hostname}</strong> as <strong>Domainnamen</strong>.
|
||||
No KarlDNS username or password is needed; if your router requires them,
|
||||
enter any placeholder.
|
||||
</p>
|
||||
{_code_window(update_url, copy_label="FRITZ!Box update URL", class_name="mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Manual updates.</h2>
|
||||
<p class="section-copy">
|
||||
Scripts can send the IP explicitly, or omit it and let KarlDNS use the request IP.
|
||||
Visible examples use your public IP when KarlDNS or your browser can detect it.
|
||||
</p>
|
||||
{_code_window(manual_commands, copy_label="manual update commands", class_name="manual-code mt-3")}
|
||||
</div>
|
||||
<div>
|
||||
<h2>Use your own DNS name.</h2>
|
||||
<p class="section-copy">
|
||||
Create a CNAME at your DNS provider that points to <strong>{hostname}</strong>.
|
||||
This is the same value as Domainnamen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
|
||||
|
||||
def _management_message(message: str | None) -> SafeHtml:
|
||||
if not message:
|
||||
return trusted("")
|
||||
return trusted(f"""
|
||||
<div class="alert alert-info mb-0 rounded-0 border-start-0 border-end-0" role="status">
|
||||
<div class="alert alert-info ui-alert mb-0 rounded-0 border-start-0 border-end-0"
|
||||
role="status" data-slot="alert">
|
||||
<div class="container">{escape(message)}</div>
|
||||
</div>
|
||||
""")
|
||||
@@ -248,10 +403,12 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en
|
||||
remove_form = ""
|
||||
if password_enabled:
|
||||
remove_form = f"""
|
||||
<form class="dashboard-card dashboard-form-card" method="post" action="/m/{attr(management_slug)}/password/remove">
|
||||
<form class="dashboard-card dashboard-form-card" data-slot="card" method="post"
|
||||
action="/m/{attr(management_slug)}/password/remove">
|
||||
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
|
||||
<label class="form-label" for="dashboard-remove-current-password">Current password</label>
|
||||
<input class="form-control" id="dashboard-remove-current-password" name="current_dashboard_password" type="password" autocomplete="current-password" required>
|
||||
<input class="form-control" id="dashboard-remove-current-password"
|
||||
name="current_dashboard_password" type="password" autocomplete="current-password" required>
|
||||
<button class="btn btn-outline-danger rounded-pill" type="submit">Remove password</button>
|
||||
</form>
|
||||
"""
|
||||
@@ -260,7 +417,8 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en
|
||||
current_password_field = """
|
||||
<div>
|
||||
<label class="form-label" for="dashboard-current-password">Current password</label>
|
||||
<input class="form-control" id="dashboard-current-password" name="current_dashboard_password" type="password" autocomplete="current-password" required>
|
||||
<input class="form-control" id="dashboard-current-password"
|
||||
name="current_dashboard_password" type="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
"""
|
||||
return trusted(f"""
|
||||
@@ -270,19 +428,26 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en
|
||||
<div>
|
||||
<p class="eyebrow">Dashboard password</p>
|
||||
<h2>Password protection</h2>
|
||||
<p class="section-copy">Status: {escape(password_status)}. This password protects only this dashboard. Router updates still work.</p>
|
||||
<p class="section-copy">
|
||||
Status: {escape(password_status)}. This password protects only this dashboard.
|
||||
Router updates still work.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-grid gap-3">
|
||||
<form class="dashboard-card dashboard-form-card" method="post" action="/m/{attr(management_slug)}/password">
|
||||
<form class="dashboard-card dashboard-form-card" data-slot="card" method="post"
|
||||
action="/m/{attr(management_slug)}/password">
|
||||
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
|
||||
{current_password_field}
|
||||
<div>
|
||||
<label class="form-label" for="dashboard-password">{escape(password_action)}</label>
|
||||
<input class="form-control" id="dashboard-password" name="dashboard_password" type="password" autocomplete="new-password" minlength="8" required>
|
||||
<input class="form-control" id="dashboard-password" name="dashboard_password"
|
||||
type="password" autocomplete="new-password" minlength="8" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="dashboard-password-confirm">Confirm password</label>
|
||||
<input class="form-control" id="dashboard-password-confirm" name="dashboard_password_confirm" type="password" autocomplete="new-password" minlength="8" required>
|
||||
<input class="form-control" id="dashboard-password-confirm"
|
||||
name="dashboard_password_confirm" type="password" autocomplete="new-password"
|
||||
minlength="8" required>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<button class="btn btn-primary rounded-pill" type="submit">{escape(password_action)}</button>
|
||||
@@ -305,7 +470,9 @@ def _render_management_unlock_page(
|
||||
hostname = escape(account["hostname"])
|
||||
message_html = ""
|
||||
if message:
|
||||
message_html = f'<div class="alert alert-warning" role="alert">{escape(message)}</div>'
|
||||
message_html = (
|
||||
f'<div class="alert alert-warning ui-alert" role="alert" data-slot="alert">{escape(message)}</div>'
|
||||
)
|
||||
return _page(
|
||||
"Unlock KarlDNS management",
|
||||
trusted(f"""
|
||||
@@ -321,11 +488,13 @@ def _render_management_unlock_page(
|
||||
<section class="section">
|
||||
<div class="container narrow">
|
||||
{message_html}
|
||||
<form class="card card-body gap-3" method="post" action="/m/{attr(management_slug)}/unlock">
|
||||
<form class="card card-body gap-3" data-slot="card" method="post"
|
||||
action="/m/{attr(management_slug)}/unlock">
|
||||
<input type="hidden" name="csrf" value="{attr(csrf_token)}">
|
||||
<div>
|
||||
<label class="form-label" for="dashboard-unlock-password">Dashboard password</label>
|
||||
<input class="form-control" id="dashboard-unlock-password" name="dashboard_password" type="password" autocomplete="current-password" required autofocus>
|
||||
<input class="form-control" id="dashboard-unlock-password" name="dashboard_password"
|
||||
type="password" autocomplete="current-password" required autofocus>
|
||||
</div>
|
||||
<label class="form-check d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="checkbox" name="stay_unlocked" value="1">
|
||||
@@ -341,15 +510,39 @@ def _render_management_unlock_page(
|
||||
|
||||
|
||||
def _user_event_row(event: Mapping[str, object]) -> str:
|
||||
created = escape(event.get("created_at") or "-")
|
||||
row = _user_event_row_view(event)
|
||||
return (
|
||||
f'<tr><td>{row.created}</td><td><span class="status-badge {attr(row.badge_class)}">{row.status}</span></td>'
|
||||
f"<td>{row.ipv4}</td><td>{row.ipv6}</td><td>{row.count}</td><td>{row.detail}</td></tr>"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _UserEventRow:
|
||||
created: str
|
||||
status: str
|
||||
badge_class: str
|
||||
ipv4: str
|
||||
ipv6: str
|
||||
count: str
|
||||
detail: str
|
||||
|
||||
|
||||
def _user_event_row_view(event: Mapping[str, object]) -> _UserEventRow:
|
||||
raw_status = str(event.get("status") or "")
|
||||
status = escape(_event_label(raw_status))
|
||||
badge_class = attr(_event_badge_class(raw_status))
|
||||
ipv4 = escape(event.get("ipv4") or "-")
|
||||
ipv6 = escape(event.get("ipv6") or "-")
|
||||
count = escape(event.get("count") or 1)
|
||||
detail = escape(event.get("detail") or "-")
|
||||
return f'<tr><td>{created}</td><td><span class="status-badge {badge_class}">{status}</span></td><td>{ipv4}</td><td>{ipv6}</td><td>{count}</td><td>{detail}</td></tr>'
|
||||
return _UserEventRow(
|
||||
created=escape(_event_text(event, "created_at")),
|
||||
status=escape(_event_label(raw_status)),
|
||||
badge_class=_event_badge_class(raw_status),
|
||||
ipv4=escape(_event_text(event, "ipv4")),
|
||||
ipv6=escape(_event_text(event, "ipv6")),
|
||||
count=escape(event.get("count") or 1),
|
||||
detail=escape(_event_text(event, "detail")),
|
||||
)
|
||||
|
||||
|
||||
def _event_text(event: Mapping[str, object], key: str) -> str:
|
||||
return str(event.get(key) or "-")
|
||||
|
||||
|
||||
def _event_badge_class(status: str) -> str:
|
||||
|
||||
Reference in new issue
Block a user