diff --git a/pikit-api.py b/pikit-api.py
index 0a3aa47..b4a9cba 100644
--- a/pikit-api.py
+++ b/pikit-api.py
@@ -174,6 +174,14 @@ def normalize_path(path: str | None) -> str:
return p
+def default_host():
+ """Return preferred hostname (append .local if bare)."""
+ host = socket.gethostname()
+ if "." not in host:
+ host = f"{host}.local"
+ return host
+
+
def dbg(msg):
# Legacy debug file logging (when /boot/pikit-debug exists)
if DEBUG_FLAG:
@@ -226,7 +234,7 @@ def load_services():
try:
data = json.loads(SERVICE_JSON.read_text())
# Normalize entries: ensure url built from port if missing
- host = socket.gethostname()
+ host = default_host()
for svc in data:
svc_path = normalize_path(svc.get("path"))
if svc_path:
@@ -506,16 +514,8 @@ def set_updates_config(opts: dict):
def detect_https(host, port):
- try:
- import ssl
- ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
- ctx.check_hostname = False
- ctx.verify_mode = ssl.CERT_NONE
- with socket.create_connection((host, int(port)), timeout=1.5) as sock:
- with ctx.wrap_socket(sock, server_hostname=host):
- return True
- except Exception:
- return False
+ """Heuristic: known HTTPS ports or .local certs."""
+ return int(port) in HTTPS_PORTS or str(host).lower().endswith(".local") or str(host).lower() == "pikit"
def factory_reset():
@@ -1176,6 +1176,12 @@ class Handler(BaseHTTPRequestHandler):
if port:
svc["online"] = port_online("127.0.0.1", port)
svc["firewall_open"] = ufw_status_allows(port)
+ # Rebuild URL with preferred host (adds .local)
+ host = default_host()
+ path = normalize_path(svc.get("path"))
+ scheme = svc.get("scheme") or ("https" if detect_https(host, port) else "http")
+ svc["scheme"] = scheme
+ svc["url"] = f"{scheme}://{host}:{port}{path}"
services.append(svc)
self._send(200, {"services": services})
elif self.path.startswith("/api/updates/auto"):
@@ -1294,7 +1300,7 @@ class Handler(BaseHTTPRequestHandler):
services = load_services()
if any(s.get("port") == port for s in services):
return self._send(400, {"error": "port already exists"})
- host = socket.gethostname()
+ host = default_host()
scheme = payload.get("scheme")
if scheme not in ("http", "https"):
scheme = "https" if detect_https(host, port) else "http"
@@ -1368,7 +1374,7 @@ class Handler(BaseHTTPRequestHandler):
svc["port"] = new_port_int
target_port = new_port_int
port_changed = True
- host = socket.gethostname()
+ host = default_host()
if new_path is not None:
path = normalize_path(new_path)
if path:
diff --git a/pikit-web/onboarding/index.html b/pikit-web/onboarding/index.html
new file mode 100644
index 0000000..79a709f
--- /dev/null
+++ b/pikit-web/onboarding/index.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+ Welcome to your Pi-Kit
+
+
+
+
+
+
+ Secure connection to your Pi-Kit
+
+
+
+ You’re on your Pi-Kit. Everything stays on your local network. Let’s switch to the
+ secure (HTTPS) dashboard.
+
+
+
+
+
+ If you see a warning
+
+ - Brave/Chrome: click Advanced → Proceed.
+ - Firefox: click Advanced → Accept the Risk & Continue.
+
+ This is safe for your own Pi on your own network.
+
+
+
+ Install the Pi-Kit CA (recommended, one-time)
+ This removes future warnings for both Pi-Kit and DietPi dashboards.
+
+ Linux (Arch/Endeavour)
+ curl -s https://pikit.local/assets/pikit-ca.crt -o /tmp/pikit-ca.crt && sudo install -m644 /tmp/pikit-ca.crt /etc/ca-certificates/trust-source/anchors/ && sudo trust extract-compat
+
+
+
+ Linux (Debian/Ubuntu)
+ curl -s https://pikit.local/assets/pikit-ca.crt -o /tmp/pikit-ca.crt && sudo cp /tmp/pikit-ca.crt /usr/local/share/ca-certificates/pikit-ca.crt && sudo update-ca-certificates
+
+
+
+ macOS
+ Double-click pikit-ca.crt → Always Trust.
+
+
+ Windows
+ Run mmc → Add/Remove Snap-in → Certificates (Computer) → Trusted Root CAs → Import pikit-ca.crt.
+
+
+
+
+
+
+
+
+
diff --git a/pikit-web/onboarding/style.css b/pikit-web/onboarding/style.css
new file mode 100644
index 0000000..a53ed7b
--- /dev/null
+++ b/pikit-web/onboarding/style.css
@@ -0,0 +1,131 @@
+:root {
+ color-scheme: dark;
+ --bg: #0c111a;
+ --panel: #131a24;
+ --text: #dce5f7;
+ --muted: #95a3c1;
+ --accent: #3dd598;
+ --border: #1f2734;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ display: grid;
+ place-items: center;
+ background: radial-gradient(120% 120% at 20% 20%, #162133, #0c111a 60%);
+ color: var(--text);
+ font-family: "DM Sans", "Inter", system-ui, -apple-system, sans-serif;
+ padding: 24px;
+}
+
+.card {
+ max-width: 720px;
+ width: 100%;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ padding: 20px 22px 24px;
+ box-shadow: 0 12px 50px rgba(0, 0, 0, 0.35);
+}
+
+header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+h1 {
+ margin: 0;
+ font-size: 1.35rem;
+}
+
+p {
+ margin: 8px 0;
+ color: var(--muted);
+}
+
+.dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: var(--accent);
+ box-shadow: 0 0 12px var(--accent);
+}
+
+.actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin: 12px 0 4px;
+}
+
+button,
+a.ghost {
+ border: 1px solid var(--border);
+ background: var(--accent);
+ color: #0c111a;
+ padding: 10px 16px;
+ border-radius: 10px;
+ font-weight: 600;
+ cursor: pointer;
+ text-decoration: none;
+}
+
+button.ghost,
+a.ghost {
+ background: transparent;
+ color: var(--text);
+}
+
+button.copy {
+ margin-left: 8px;
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--border);
+ padding: 6px 10px;
+}
+
+.steps {
+ margin: 12px 0;
+ padding: 10px 12px;
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.steps h3 {
+ margin: 0 0 6px;
+}
+
+ul {
+ margin: 6px 0 4px 18px;
+ color: var(--text);
+}
+
+code {
+ display: block;
+ background: #0b1018;
+ border: 1px solid var(--border);
+ padding: 10px;
+ border-radius: 10px;
+ margin-top: 6px;
+ color: var(--text);
+ word-break: break-all;
+}
+
+summary {
+ cursor: pointer;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.footnote {
+ font-size: 0.9rem;
+ color: var(--muted);
+}