Add formatting and type checks

This commit is contained in:
StasonJatham committed 2026-05-22 17:05:06 +02:00
1 parent c0d82eaf26
commit 24747866bc
38 files changed
+800 -265

No files matched your search

+18 -26
View File
@@ -46,8 +46,7 @@ def _copy_row(label: str, value: str, sensitivity: str | None = None, *, mask_se
else ""
)
row_class = "copy-field copy-field-secret" if is_secret else "copy-field copy-field-public"
return trusted(
f"""
return trusted(f"""
<div class="{row_class}">
<div class="copy-pill">
<span class="copy-pill-label">{safe_label}</span>
@@ -58,8 +57,7 @@ def _copy_row(label: str, value: str, sensitivity: str | None = None, *, mask_se
{_svg_icon("clipboard")}<span data-copy-label>Copy</span>{_svg_icon("check", "check-icon")}
</button>
</div>
"""
)
""")
def _copy_button(label: str, value: str, *, style: str = "outline-primary") -> SafeHtml:
@@ -89,8 +87,7 @@ def _copy_menu(label: str, items: list[tuple[str, str]]) -> SafeHtml:
f'<button type="button" class="copy-menu-item" data-copy="{attr(item_value)}" aria-label="Copy {attr(item_label)}">'
f'{_svg_icon("clipboard")}<span data-copy-label>{escape(item_label)}</span>{_svg_icon("check", "check-icon")}</button>'
)
return trusted(
f"""
return trusted(f"""
<details class="copy-menu">
<summary aria-label="{safe_label}" title="{safe_label}">
{_svg_icon("more", "more-icon")}
@@ -99,22 +96,21 @@ def _copy_menu(label: str, items: list[tuple[str, str]]) -> SafeHtml:
{''.join(buttons)}
</div>
</details>
"""
)
""")
def _code_window(commands: str, *, copy_label: str = "code", class_name: str = "", copy_value: str | None = None) -> SafeHtml:
def _code_window(
commands: str, *, copy_label: str = "code", class_name: str = "", copy_value: str | None = None
) -> SafeHtml:
safe_class = f" {attr(class_name)}" if class_name else ""
code_lines = "\n".join(f"<code>{escape(line)}</code>" for line in commands.splitlines())
copy_value = copy_value if copy_value is not None else _code_copy_value(commands)
return trusted(
f"""
return trusted(f"""
<div class="terminal-card code-window{safe_class}">
{_copy_icon_button(copy_label, copy_value)}
{code_lines}
</div>
"""
)
""")
def _code_copy_value(commands: str) -> str:
@@ -167,8 +163,7 @@ def _known_request_ip(request_ip: str | None = None) -> tuple[int | None, str]:
def _manual_update_panel(commands: str) -> SafeHtml:
return trusted(
f"""
return trusted(f"""
<div class="card card-body gap-3 mt-4">
<div>
<h3>Manual updates</h3>
@@ -176,8 +171,7 @@ def _manual_update_panel(commands: str) -> SafeHtml:
</div>
{_code_window(commands, copy_label="manual update commands", class_name="manual-code")}
</div>
"""
)
""")
def _fritzbox_fields(
@@ -196,25 +190,23 @@ def _fritzbox_fields(
def _secret_notice(*, include_registration: bool = False) -> SafeHtml:
secret_items = "Update URL, register URL, and dashboard link" if include_registration else "Update URL and dashboard link"
return trusted(
f"""
secret_items = (
"Update URL, register URL, and dashboard link" if include_registration else "Update URL and dashboard link"
)
return trusted(f"""
<div class="alert alert-info my-3 py-2 small" role="note">
<strong>Public:</strong> Hostname and CNAME target can be shared.
<strong>Secret:</strong> {escape(secret_items)} can update or manage this hostname.
Secrets are hidden on screen. Copy still copies the real value.
</div>
"""
)
""")
def _message_band(message: str | None) -> SafeHtml:
if not message:
return trusted("")
return trusted(
f"""
return trusted(f"""
<div class="alert alert-warning mb-0 rounded-0 border-start-0 border-end-0" role="status">
<div class="container">{escape(message)}</div>
</div>
"""
)
""")
+3 -1
View File
@@ -17,7 +17,9 @@ def parse_form(body: bytes) -> dict[str, list[str]]:
try:
return urllib.parse.parse_qs(decoded, keep_blank_values=True, max_num_fields=MAX_FORM_FIELDS)
except ValueError as exc:
raise HTTPException(status_code=400, detail=f"form must not contain more than {MAX_FORM_FIELDS} fields") from exc
raise HTTPException(
status_code=400, detail=f"form must not contain more than {MAX_FORM_FIELDS} fields"
) from exc
async def request_form(request: Request, settings: DdnsSettings) -> dict[str, list[str]]:
+15 -11
View File
@@ -18,8 +18,7 @@ def _top_nav(label: str | None = None, links: SafeHtml | None = None) -> SafeHtm
{_svg_icon("sun", "theme-icon theme-icon-sun")}
</button>
"""
return trusted(
f"""
return trusted(f"""
<a class="skip-link" href="#main">Skip to content</a>
<nav class="navbar navbar-expand-sm site-nav" aria-label="Main navigation">
<div class="container">
@@ -31,13 +30,11 @@ def _top_nav(label: str | None = None, links: SafeHtml | None = None) -> SafeHtm
</div>
</div>
</nav>
"""
)
""")
def _site_footer() -> SafeHtml:
return trusted(
f"""
return trusted(f"""
<footer class="site-footer border-top">
<div class="container footer-inner">
<div class="footer-made">Made with <span aria-label="love">❤️</span> by karl</div>
@@ -65,8 +62,7 @@ def _site_footer() -> SafeHtml:
</nav>
</div>
</footer>
"""
)
""")
def _page(
@@ -85,7 +81,11 @@ def _page(
raise TypeError("body must be a SafeHtml fragment built by a template helper")
extra_styles_html = extra_styles or trusted("")
extra_scripts_html = extra_scripts or trusted("")
preload_logo_html = trusted('<link rel="preload" as="image" href="/logo.webp?v=3" type="image/webp">') if preload_logo else trusted("")
preload_logo_html = (
trusted('<link rel="preload" as="image" href="/logo.webp?v=3" type="image/webp">')
if preload_logo
else trusted("")
)
canonical_html = trusted(f'<link rel="canonical" href="{attr(canonical_url)}">') if canonical_url else trusted("")
social_url = canonical_url or ""
social_image = image_url or ""
@@ -93,14 +93,18 @@ def _page(
if social_url:
social_card = "summary_large_image" if social_image else "summary"
social_image_alt = image_alt or title
social_image_html = f"""
social_image_html = (
f"""
<meta property="og:image" content="{attr(social_image)}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="{attr(social_image_alt)}">
<meta name="twitter:image" content="{attr(social_image)}">
<meta name="twitter:image:alt" content="{attr(social_image_alt)}">
""" if social_image else ""
"""
if social_image
else ""
)
social_html = trusted(f"""
<meta property="og:type" content="website">
<meta property="og:title" content="{attr(title)}">
-1
View File
@@ -47,7 +47,6 @@ class RequestBodyLimitMiddleware:
await response(scope, receive, send)
def install_security_middleware(app: FastAPI, settings: DdnsSettings, store: DdnsStore) -> None:
@app.middleware("http")
async def security_middleware(request: Request, call_next):
+35 -22
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping, Sequence
from datetime import datetime
from urllib.parse import urlencode
@@ -30,13 +31,14 @@ def _admin_secret_panel(title: str, rows: list[SafeHtml]) -> SafeHtml:
</section>
""")
def _render_admin_page(
service: DdnsService,
settings: DdnsSettings,
accounts: list[dict[str, str | int | None]],
cleanup_runs: list[dict[str, str | int]],
scheduled_tasks: list[dict[str, str | int | None]],
update_events: list[dict[str, str | int | None]],
accounts: Sequence[Mapping[str, object]],
cleanup_runs: Sequence[Mapping[str, object]],
scheduled_tasks: Sequence[Mapping[str, object]],
update_events: Sequence[Mapping[str, object]],
service_metrics: dict[str, float],
table_counts: dict[str, int],
table_offsets: dict[str, int],
@@ -55,8 +57,16 @@ def _render_admin_page(
accounts_count = escape(table_counts.get("accounts", len(accounts)))
event_count = escape(table_counts.get("update_events", len(update_events)))
metrics_html = _metrics_panel(service_metrics)
account_pager = _server_page_links(admin_path, "accounts_offset", table_offsets.get("accounts", 0), table_limit, table_counts.get("accounts", 0))
event_pager = _server_page_links(admin_path, "update_events_offset", table_offsets.get("update_events", 0), table_limit, table_counts.get("update_events", 0))
account_pager = _server_page_links(
admin_path, "accounts_offset", table_offsets.get("accounts", 0), table_limit, table_counts.get("accounts", 0)
)
event_pager = _server_page_links(
admin_path,
"update_events_offset",
table_offsets.get("update_events", 0),
table_limit,
table_counts.get("update_events", 0),
)
cleanup_panel = _cleanup_panel(settings, cleanup_runs, scheduled_tasks, safe_admin_path, csrf)
totals_html = _totals_strip(table_counts, service_metrics)
return _page(
@@ -142,10 +152,7 @@ def _totals_strip(table_counts: dict[str, int], metrics: dict[str, float]) -> Sa
]
return trusted(
'<div class="admin-total-strip" aria-label="Service totals">'
+ "".join(
f'<span><strong>{escape(value)}</strong>{escape(label)}</span>'
for label, value in items
)
+ "".join(f"<span><strong>{escape(value)}</strong>{escape(label)}</span>" for label, value in items)
+ "</div>"
)
@@ -181,12 +188,15 @@ def _server_page_links(admin_path: str, offset_name: str, offset: int, limit: in
def _cleanup_panel(
settings: DdnsSettings,
cleanup_runs: list[dict[str, str | int]],
scheduled_tasks: list[dict[str, str | int | None]],
cleanup_runs: Sequence[Mapping[str, object]],
scheduled_tasks: Sequence[Mapping[str, object]],
safe_admin_path: str,
csrf: str,
) -> SafeHtml:
task = next((item for item in scheduled_tasks if item.get("name") == "cleanup"), scheduled_tasks[0] if scheduled_tasks else {})
task = next(
(item for item in scheduled_tasks if item.get("name") == "cleanup"),
scheduled_tasks[0] if scheduled_tasks else {},
)
last_run = task.get("last_finished_at") or (cleanup_runs[0].get("created_at") if cleanup_runs else None)
next_run = task.get("next_run_at")
status = task.get("last_status") or "pending"
@@ -235,14 +245,14 @@ def _metrics_panel(metrics: dict[str, float]) -> SafeHtml:
("Blocked requests", _metric_int(metrics.get("rate_limit_hits_total", 0))),
("Average update time", f"{average_latency:.1f} ms"),
("Last update time", f"{metrics.get('last_update_latency_ms', 0):.1f} ms"),
("Cleanup ok/errors", f"{_metric_int(metrics.get('cleanup_success_total', 0))}/{_metric_int(metrics.get('cleanup_errors_total', 0))}"),
(
"Cleanup ok/errors",
f"{_metric_int(metrics.get('cleanup_success_total', 0))}/{_metric_int(metrics.get('cleanup_errors_total', 0))}",
),
]
cards = "\n".join(
f"""
cards = "\n".join(f"""
<span><strong>{escape(value)}</strong>{escape(label)}</span>
"""
for label, value in items
)
""" for label, value in items)
return trusted(f"""
<div class="admin-metric-strip" aria-label="Service metrics">
{cards}
@@ -250,13 +260,16 @@ def _metrics_panel(metrics: dict[str, float]) -> SafeHtml:
""")
def _metric_int(value: float) -> str:
return str(int(value))
def _metric_int(value: object) -> str:
try:
return str(int(float(str(value))))
except ValueError:
return "0"
def _format_interval(seconds: object) -> str:
try:
value = int(seconds)
value = int(str(seconds))
except (TypeError, ValueError):
return str(seconds)
if value % 3600 == 0:
+3 -2
View File
@@ -1,12 +1,13 @@
from __future__ import annotations
from collections.abc import Mapping
from datetime import datetime
from .components import _event_label
from .html_utils import attr, escape
def _account_row(account: dict[str, str | int | None], csrf: str = "", admin_path: str = "/admin") -> str:
def _account_row(account: Mapping[str, object], csrf: str = "", admin_path: str = "/admin") -> str:
raw_hostname = str(account["hostname"])
hostname = escape(raw_hostname)
hostname_attr = attr(raw_hostname)
@@ -77,7 +78,7 @@ def _admin_table_pagination() -> str:
"""
def _event_row(event: dict[str, str | int | None]) -> str:
def _event_row(event: Mapping[str, object]) -> str:
created_raw = str(event.get("created_at") or "-")
hostname_raw = str(event.get("hostname") or "-")
detail_raw = str(event.get("detail") or "-")
+21 -14
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
from collections.abc import Mapping
from ..services.ddns import DdnsService
from ..settings import DdnsSettings
from .components import (
@@ -19,7 +21,7 @@ def _render_management_page(
service: DdnsService,
settings: DdnsSettings,
management_slug: str,
account: dict[str, str | int | None],
account: Mapping[str, object],
csrf_token: str,
message: str | None = None,
request_ip: str | None = None,
@@ -37,11 +39,19 @@ def _render_management_page(
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 """
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>
"""
)
message_html = _management_message(message)
dashboard_password_panel = _dashboard_password_panel(management_slug, csrf_token, bool(account.get("dashboard_password_hash")))
dashboard_password_panel = _dashboard_password_panel(
management_slug, csrf_token, bool(account.get("dashboard_password_hash"))
)
return _page(
"KarlDNS management",
trusted(f"""
@@ -143,11 +153,12 @@ def _render_management_page(
extra_styles=trusted('<link href="/admin.css?v=5" rel="stylesheet">'),
)
def _render_pending_management_page(
service: DdnsService,
settings: DdnsSettings,
management_slug: str,
account: dict[str, str | int | None],
account: Mapping[str, object],
csrf_token: str,
message: str | None = None,
request_ip: str | None = None,
@@ -224,13 +235,11 @@ def _render_pending_management_page(
def _management_message(message: str | None) -> SafeHtml:
if not message:
return trusted("")
return trusted(
f"""
return trusted(f"""
<div class="alert alert-info mb-0 rounded-0 border-start-0 border-end-0" role="status">
<div class="container">{escape(message)}</div>
</div>
"""
)
""")
def _dashboard_password_panel(management_slug: str, csrf_token: str, password_enabled: bool) -> SafeHtml:
@@ -254,8 +263,7 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en
<input class="form-control" id="dashboard-current-password" name="current_dashboard_password" type="password" autocomplete="current-password" required>
</div>
"""
return trusted(
f"""
return trusted(f"""
<section class="dashboard-section">
<div class="container">
<div class="dashboard-settings-grid">
@@ -285,13 +293,12 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en
</div>
</div>
</section>
"""
)
""")
def _render_management_unlock_page(
management_slug: str,
account: dict[str, str | int | None],
account: Mapping[str, object],
csrf_token: str,
message: str | None = None,
) -> str:
@@ -333,7 +340,7 @@ def _render_management_unlock_page(
)
def _user_event_row(event: dict[str, str | int | None]) -> str:
def _user_event_row(event: Mapping[str, object]) -> str:
created = escape(event.get("created_at") or "-")
raw_status = str(event.get("status") or "")
status = escape(_event_label(raw_status))
+4 -1
View File
@@ -99,6 +99,7 @@ def _render_public_home(
},
)
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."
@@ -159,7 +160,9 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
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}'"
register_example = (
f"curl -sS -X POST '{base_url}/api/v1/router/register/<karl_dashboard_secret>?myip={example_ipv4}'"
)
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"
+2 -4
View File
@@ -8,8 +8,7 @@ def _linux_installer_section(base_url: str) -> SafeHtml:
base_url = base_url.rstrip("/")
system_install = f"curl -fsSL {base_url}/install.sh | sudo sh"
system_uninstall = f"curl -fsSL {base_url}/uninstall.sh | sudo sh"
return trusted(
f"""
return trusted(f"""
<section class="section" id="linux-installer">
<div class="container narrow">
<div class="text-center mb-4">
@@ -31,5 +30,4 @@ def _linux_installer_section(base_url: str) -> SafeHtml:
</div>
</div>
</section>
"""
)
""")