From b9ec4e57ff5a4b8bb757213b23f43036357bdcd8 Mon Sep 17 00:00:00 2001 From: StasonJatham Date: Fri, 22 May 2026 16:27:25 +0200 Subject: [PATCH] Refine management dashboard layout --- karldns/web/styles.py | 281 ++++++++++++++++++++++++++++ karldns/web/templates_management.py | 168 +++++++++-------- 2 files changed, 369 insertions(+), 80 deletions(-) diff --git a/karldns/web/styles.py b/karldns/web/styles.py index bb063ac..5ddcc01 100644 --- a/karldns/web/styles.py +++ b/karldns/web/styles.py @@ -1323,6 +1323,287 @@ td { overflow-wrap: anywhere; } +.dashboard-page { + background: var(--rp-canvas); +} + +.dashboard-shell { + padding: 32px 0 18px; +} + +.dashboard-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + margin-bottom: 18px; +} + +.dashboard-header h1 { + margin-top: 4px; + font-size: 32px; +} + +.dashboard-page .lead { + margin-top: 8px; + font-size: 15px; +} + +.dashboard-page .section-copy { + margin-top: 6px; +} + +.dashboard-page .alert { + margin: 0 0 18px; + border-radius: 12px; +} + +.dashboard-grid { + display: grid; + grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.75fr); + gap: 16px; + align-items: start; +} + +.dashboard-card { + min-width: 0; + padding: 18px; + border: 1px solid var(--rp-line); + border-radius: 12px; + background: var(--rp-surface); +} + +.dashboard-card-main { + display: grid; + gap: 12px; +} + +.dashboard-card-compact { + display: grid; + gap: 10px; + margin-top: 16px; +} + +.dashboard-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.dashboard-card-header h2, +.dashboard-section h2 { + margin-top: 2px; + font-size: 22px; +} + +.dashboard-copy-stack { + display: grid; + gap: 8px; +} + +.dashboard-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.dashboard-page .copy-field { + gap: 8px; +} + +.dashboard-page .copy-pill { + min-height: 48px; + border-radius: 12px; +} + +.dashboard-page .copy-button { + min-width: 96px; + min-height: 48px; +} + +.dashboard-page .copy-field code, +.dashboard-page .copy-field input { + font-size: 14px; +} + +.dashboard-page .copy-reveal-button { + min-height: 32px; +} + +.dashboard-status-card { + display: grid; + gap: 12px; +} + +.dashboard-status-list { + display: grid; + gap: 0; + margin: 0; + border: 1px solid var(--rp-soft-line); + border-radius: 10px; + overflow: hidden; +} + +.dashboard-status-list div { + display: grid; + grid-template-columns: 82px minmax(0, 1fr); + border-bottom: 1px solid var(--rp-soft-line); +} + +.dashboard-status-list div:last-child { + border-bottom: 0; +} + +.dashboard-status-list dt, +.dashboard-status-list dd { + min-width: 0; + margin: 0; + padding: 10px 12px; + overflow-wrap: anywhere; +} + +.dashboard-status-list dt { + color: var(--rp-muted); + background: var(--rp-surface-alt); + font-size: 13px; + font-weight: 600; +} + +.dashboard-status-list dd { + font: 500 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.dashboard-section { + padding: 24px 0 0; +} + +.dashboard-section-header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 10px; +} + +.dashboard-settings-grid { + display: grid; + grid-template-columns: minmax(240px, 0.75fr) minmax(0, 1.25fr); + gap: 20px; + align-items: start; +} + +.dashboard-form-card { + display: grid; + gap: 12px; +} + +.dashboard-table-shell { + max-height: 360px; + overflow: auto; + border: 1px solid var(--rp-line); + border-radius: 12px; + background: var(--rp-surface); + -webkit-overflow-scrolling: touch; +} + +.dashboard-history-table { + min-width: 760px; +} + +.dashboard-history-table thead th { + position: sticky; + top: 0; + z-index: 1; + background: var(--rp-surface-alt); +} + +.dashboard-history-table th, +.dashboard-history-table td { + padding: 8px 10px; +} + +.status-badge { + display: inline-flex; + align-items: center; + min-height: 24px; + padding: 3px 9px; + border: 1px solid var(--rp-line); + border-radius: 999px; + background: var(--rp-field); + color: var(--rp-charcoal); + font-size: 12px; + font-weight: 600; + white-space: nowrap; +} + +.status-badge-ok { + color: var(--rp-success); +} + +.status-badge-error { + color: var(--rp-danger); +} + +.dashboard-danger-section { + padding-bottom: 32px; +} + +.dashboard-danger-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 18px; + border: 1px solid var(--rp-line); + border-radius: 12px; + background: var(--rp-surface); +} + +@media (max-width: 860px) { + .dashboard-shell { + padding-top: 24px; + } + + .dashboard-header, + .dashboard-card-header, + .dashboard-danger-card { + align-items: stretch; + flex-direction: column; + } + + .dashboard-grid, + .dashboard-settings-grid { + grid-template-columns: 1fr; + } + + .dashboard-header h1 { + font-size: 28px; + } +} + +@media (max-width: 560px) { + .dashboard-card, + .dashboard-danger-card { + padding: 14px; + } + + .dashboard-page .copy-button, + .dashboard-actions .btn, + .dashboard-card-header .btn, + .dashboard-danger-card .btn { + width: 100%; + } + + .dashboard-page .copy-field { + grid-template-columns: 1fr; + } + + .dashboard-status-list div { + grid-template-columns: 92px minmax(0, 1fr); + } +} + tr:last-child th, tr:last-child td { border-bottom: 0; diff --git a/karldns/web/templates_management.py b/karldns/web/templates_management.py index fb19f41..ab01e44 100644 --- a/karldns/web/templates_management.py +++ b/karldns/web/templates_management.py @@ -4,12 +4,11 @@ from ..services.ddns import DdnsService from ..settings import DdnsSettings from .components import ( _code_window, - _copy_menu, + _copy_button, _copy_row, _event_label, _fritzbox_fields, _manual_update_commands, - _manual_update_panel, _secret_notice, ) from .html_utils import SafeHtml, attr, escape, trusted @@ -46,98 +45,97 @@ def _render_management_page( return _page( "KarlDNS management", trusted(f""" -
+
{_top_nav()} -
-
-

Dashboard

-

{hostname}

-

This private link can manage this hostname. Keep it safe.

- Home -
-
- {message_html} -
+
-
-
-

Router settings

-

Router update settings

-

Use this Update-URL in your router or script. Optional: point another DNS name at this hostname with a CNAME.

- {_secret_notice()} -
- {_copy_menu("Copy setup values", [("Router fields", fritzbox_fields), ("CNAME", cname_record)])} -
-
-
- {_copy_row("Update-URL:", update_url, "Secret")} - {_copy_row("Domainnamen:", hostname, "Public")} - {_copy_row("CNAME target:", hostname, "Public")} -
-
- {_manual_update_panel(manual_commands)} -
+
-

Actions

-

Replace router update URL

-

Create a new update URL. Update your router after changing it.

-
-
-
- - - -
+

Dashboard

+

{hostname}

+

Private management link. Keep it safe.

+ Home
+ {message_html} +
+
+
+
+

Router settings

+

Update endpoint

+
+
+ + + +
+
+

Paste the Update-URL into your router. Use the hostname as Domainnamen, or point your own subdomain at it with a CNAME.

+ {_secret_notice()} +
+ {_copy_row("Update-URL:", update_url, "Secret")} + {_copy_row("Domainnamen:", hostname, "Public")} + {_copy_row("CNAME target:", hostname, "Public")} +
+
+ {_copy_button("Router fields", fritzbox_fields)} + {_copy_button("Copy CNAME", cname_record, style="outline-secondary")} +
+
+
+

Current address

+

Status

+
+
IPv4
{ipv4}
+
IPv6
{ipv6}
+
Updated
{updated}
+
+
+
+
+
+
+

Manual updates

+

curl examples

+
+
+

FRITZ!Box fills IP placeholders automatically. Scripts can send an IP explicitly or omit it and let KarlDNS use the request IP.

+ {_code_window(manual_commands, copy_label="manual update commands", class_name="manual-code")} +
{dashboard_password_panel} -
-
-
-

Current address

-

Status

-
-
- - - - - - -
IPv4{ipv4}
IPv6{ipv6}
Updated{updated}
-
-
-
-
+
-
+

History

Router updates

Recent IP changes and failed updates for this hostname.

-
- +
+
{history_rows}
TimeStatusIPv4IPv6CountDetail
-
-
-
-

Delete

-

Delete hostname

-

This removes the hostname and DNS records. The router update URL will stop working.

+
+
+
+
+

Delete

+

Delete hostname

+

Remove this hostname and its DNS records. The update URL will stop working.

+
+
+ + +
-
- - -
@@ -241,7 +239,7 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en remove_form = "" if password_enabled: remove_form = f""" -
+ @@ -258,16 +256,16 @@ def _dashboard_password_panel(management_slug: str, csrf_token: str, password_en """ return trusted( f""" -
+
-
-
+
+

Dashboard password

Password protection

Status: {escape(password_status)}. This password protects only this dashboard. Router updates still work.

-
- +
+ {current_password_field}
@@ -337,9 +335,19 @@ def _render_management_unlock_page( def _user_event_row(event: dict[str, str | int | None]) -> str: created = escape(event.get("created_at") or "-") - status = escape(_event_label(event.get("status"))) + 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"{created}{status}{ipv4}{ipv6}{count}{detail}" + return f'{created}{status}{ipv4}{ipv6}{count}{detail}' + + +def _event_badge_class(status: str) -> str: + if status in {"updated", "dashboard_hostname_activated", "unchanged"}: + return "status-badge-ok" + if "failed" in status or "failure" in status: + return "status-badge-error" + return "status-badge-neutral"