Polish KarlDNS production UI and docs

This commit is contained in:
StasonJatham committed 2026-05-21 10:05:40 +02:00
1 parent ca92dc211a
commit 5beee0e5c6
61 files changed
+1566 -523

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_OUTPUT = ROOT / "router_dyndns" / "assets" / "bootstrap-subset.css"
SCAN_PATHS = [
ROOT / "router_dyndns" / "web",
ROOT / "router_dyndns" / "routers",
]
CSS = r"""
:root{--bs-body-color:#000;--bs-body-bg:#fff;--bs-border-color:#e5e5e5;--bs-secondary-color:#737373}[data-bs-theme="dark"]{--bs-body-color:#f5f5f5;--bs-body-bg:#0f0f0f;--bs-border-color:rgba(255,255,255,.16);--bs-secondary-color:#a3a3a3}html{-webkit-text-size-adjust:100%}body{margin:0}button,input,select{margin:0;font:inherit}button,select{text-transform:none}button:not(:disabled),[type="button"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}a{color:inherit}img,svg{vertical-align:middle}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}pre{margin:0}.container{width:100%;margin-right:auto;margin-left:auto;padding-right:12px;padding-left:12px}.row{display:flex;flex-wrap:wrap;margin-right:calc(-.5 * var(--bs-gutter-x,0));margin-left:calc(-.5 * var(--bs-gutter-x,0));margin-top:calc(-1 * var(--bs-gutter-y,0))}.row>*{width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x,0) * .5);padding-left:calc(var(--bs-gutter-x,0) * .5);margin-top:var(--bs-gutter-y,0)}.col-6{flex:0 0 auto;width:50%}.col-12{flex:0 0 auto;width:100%}@media (min-width:768px){.col-md-6{flex:0 0 auto;width:50%}.align-items-md-center{align-items:center!important}.flex-md-row{flex-direction:row!important}}@media (min-width:992px){.col-lg-3{flex:0 0 auto;width:25%}.col-lg-5{flex:0 0 auto;width:41.666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.333333%}.flex-lg-row{flex-direction:row!important}}@media (min-width:1200px){.col-xl-3{flex:0 0 auto;width:25%}}.g-3{--bs-gutter-x:16px;--bs-gutter-y:16px}.g-4{--bs-gutter-x:24px;--bs-gutter-y:24px}.d-flex{display:flex!important}.d-grid{display:grid!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-wrap{flex-wrap:wrap!important}.flex-shrink-0{flex-shrink:0!important}.align-items-center{align-items:center!important}.align-items-start{align-items:flex-start!important}.align-items-stretch{align-items:stretch!important}.align-self-start{align-self:flex-start!important}.justify-content-between{justify-content:space-between!important}.justify-content-center{justify-content:center!important}.justify-content-end{justify-content:flex-end!important}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.gap-sm-3{gap:16px!important}}.gap-2{gap:8px!important}.gap-3{gap:16px!important}.gap-4{gap:24px!important}.m-0{margin:0!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.ms-auto{margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-3{margin-top:16px!important;margin-bottom:16px!important}.mt-1{margin-top:4px!important}.mt-2{margin-top:8px!important}.mt-3{margin-top:16px!important}.mt-4{margin-top:24px!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:4px!important}.mb-2{margin-bottom:8px!important}.mb-3{margin-bottom:16px!important}.mb-4{margin-bottom:24px!important}.p-0{padding:0!important}.p-4{padding:24px!important}.px-0{padding-right:0!important;padding-left:0!important}.px-2{padding-right:8px!important;padding-left:8px!important}.px-3{padding-right:16px!important;padding-left:16px!important}.py-2{padding-top:8px!important;padding-bottom:8px!important}.w-100{width:100%!important}.h-100{height:100%!important}.text-center{text-align:center!important}.text-start{text-align:left!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-secondary{color:var(--bs-secondary-color)!important}.text-danger{color:var(--rp-danger)!important}.small{font-size:.875em!important}.h5{font-size:1.25rem}.opacity-75{opacity:.75!important}.rounded{border-radius:12px!important}.rounded-0{border-radius:0!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:9999px!important}.border{border:1px solid var(--bs-border-color)!important}.border-top{border-top:1px solid var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-end-0{border-right:0!important}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:5px;padding-bottom:5px;margin-right:16px;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-text{padding-top:8px;padding-bottom:8px}.nav-link{display:block;padding:8px;text-decoration:none}.btn{--bs-btn-color:#000;--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-btn-color);--bs-btn-hover-bg:var(--bs-btn-bg);--bs-btn-hover-border-color:var(--bs-btn-border-color);--bs-btn-active-color:var(--bs-btn-hover-color);--bs-btn-active-bg:var(--bs-btn-hover-bg);--bs-btn-active-border-color:var(--bs-btn-hover-border-color);color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border:1px solid var(--bLine truncated
""".strip()
SUPPORTED_DYNAMIC_CLASSES = {"active", "disabled", "page-item", "page-link"}
def _class_names_from_css(css: str) -> set[str]:
return set(re.findall(r"\.([_a-zA-Z][_a-zA-Z0-9-]*)", css))
def _class_names_from_templates() -> set[str]:
class_names: set[str] = set()
for scan_path in SCAN_PATHS:
for path in scan_path.glob("*.py"):
text = path.read_text()
for match in re.finditer(r"class=[\"']([^\"']+)[\"']", text):
for class_name in match.group(1).split():
if "{" not in class_name and "}" not in class_name:
class_names.add(class_name)
return class_names
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Build and validate the tiny KarlDNS Bootstrap compatibility subset.")
parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT)
return parser.parse_args()
def main() -> int:
args = parse_args()
output = args.output if args.output.is_absolute() else ROOT / args.output
app_styles = (ROOT / "router_dyndns" / "web" / "styles.py").read_text()
supported = _class_names_from_css(CSS) | _class_names_from_css(app_styles) | SUPPORTED_DYNAMIC_CLASSES
unsupported = sorted(_class_names_from_templates() - supported)
if unsupported:
unsupported_text = "\n".join(f"- {class_name}" for class_name in unsupported)
raise SystemExit(f"Unsupported Bootstrap-style classes in templates:\n{unsupported_text}")
output.write_text(CSS + "\n")
print(f"Wrote {output.relative_to(ROOT)} ({output.stat().st_size} bytes)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+9 -9
View File
@@ -22,12 +22,12 @@ Usage:
Options:
--base-url URL KarlDNS server URL. Default: https://karldns.de
--raw-base-url URL Script download base URL. Default: GitHub raw main branch
--every MINUTES Cron interval, 1-60. Default: 5
--raw-base-url URL Updater script download URL
--every MINUTES Update interval, 1-60. Default: 5
--mode MODE auto, system, or user. Default: auto
--update-url URL Existing private /u/<secret> update URL
--hostname NAME Optional display hostname for output/config
--dashboard-url URL Optional private dashboard URL for output/config
--update-url URL Existing private update URL
--hostname NAME Hostname to show after install
--dashboard-url URL Private dashboard URL to show after install
--ipv4 VALUE auto, off, or a static IPv4. Default: auto
--ipv6 VALUE auto, off, or a static IPv6. Default: auto
--no-cron Install files only, do not configure cron
@@ -202,7 +202,7 @@ case "$KARLDNS_INSTALL_MODE" in
esac
if [ "$KARLDNS_INSTALL_MODE" = "system" ] && [ "$(id -u)" -ne 0 ]; then
echo "System install needs root. Re-run with sudo or set KARLDNS_INSTALL_MODE=user." >&2
echo "System install needs root. Re-run with sudo or use --mode user." >&2
exit 1
fi
@@ -215,7 +215,7 @@ if [ -z "$KARLDNS_UPDATE_URL" ]; then
fi
if [ -z "$KARLDNS_UPDATE_URL" ]; then
echo "KarlDNS installer: could not get an update URL" >&2
echo "KarlDNS installer: could not create an update URL" >&2
exit 1
fi
case "$KARLDNS_UPDATE_URL" in
@@ -230,10 +230,10 @@ fi
echo "KarlDNS installed."
echo "Hostname: ${KARLDNS_HOSTNAME:-already configured}"
echo "Dashboard: ${KARLDNS_MANAGEMENT_URL:-use your existing private status page}"
echo "Dashboard: ${KARLDNS_MANAGEMENT_URL:-use your existing private dashboard}"
echo "Mode: $KARLDNS_INSTALL_MODE"
if [ "$KARLDNS_SKIP_CRON" = "0" ]; then
echo "Interval: every $KARLDNS_INTERVAL_MINUTES minute(s)"
echo "Interval: every $KARLDNS_INTERVAL_MINUTES minutes"
else
echo "Cron: skipped"
fi
+4 -4
View File
@@ -8,7 +8,7 @@ if [ -r "$CONFIG_FILE" ]; then
. "$CONFIG_FILE"
fi
: "${KARLDNS_UPDATE_URL:?KARLDNS_UPDATE_URL is required in $CONFIG_FILE or the environment}"
: "${KARLDNS_UPDATE_URL:?Set KARLDNS_UPDATE_URL in $CONFIG_FILE or the environment}"
KARLDNS_BASE_URL="${KARLDNS_BASE_URL:-https://karldns.de}"
KARLDNS_LOCK_FILE="${KARLDNS_LOCK_FILE:-/tmp/karldns-updater.lock}"
KARLDNS_CACHE_FILE="${KARLDNS_CACHE_FILE:-${XDG_CACHE_HOME:-$HOME/.cache}/karldns/last-ip}"
@@ -49,8 +49,8 @@ elif [ "$KARLDNS_IPV6" != "off" ]; then
ipv6="$KARLDNS_IPV6"
fi
case "$ipv4" in *" "*) echo "KarlDNS: invalid IPv4 discovery result" >&2; exit 1 ;; esac
case "$ipv6" in *" "*) echo "KarlDNS: invalid IPv6 discovery result" >&2; exit 1 ;; esac
case "$ipv4" in *" "*) echo "KarlDNS: could not read a valid IPv4 address" >&2; exit 1 ;; esac
case "$ipv6" in *" "*) echo "KarlDNS: could not read a valid IPv6 address" >&2; exit 1 ;; esac
public_ip="${ipv4}|${ipv6}"
has_discovered_ip=0
if [ -n "$ipv4" ] || [ -n "$ipv6" ]; then
@@ -87,7 +87,7 @@ case "$response" in
echo "KarlDNS: $response"
;;
*)
echo "KarlDNS: update failed: $response" >&2
echo "KarlDNS: update failed. Server said: $response" >&2
exit 1
;;
esac
+2 -2
View File
@@ -13,7 +13,7 @@ Usage:
Options:
--mode MODE auto, system, or user. Default: auto
--delete-config Also delete the private updater config and last-IP cache
--delete-config Also delete saved private URLs and the last-IP cache
-h, --help Show this help
EOF
}
@@ -80,5 +80,5 @@ fi
echo "KarlDNS uninstalled from $KARLDNS_INSTALL_MODE mode."
if [ "$KARLDNS_DELETE_CONFIG" = "0" ]; then
echo "Private config was kept. Re-run with --delete-config to remove it."
echo "Saved private URLs were kept. Re-run with --delete-config to remove them."
fi