feat: GL kit-connect ipk + one-button wizard + web dashboard for both platforms

Synology kit-connect (SPK):
- wizard.sh — one-button Keylink IT fleet setup (auto-runs on install)
- www/index.html — dark-themed local dashboard (same design as busrouter)
- bin/serve-dashboard.sh — busybox httpd on port 8089 with CGI endpoints
- start-stop-status updated to manage dashboard alongside daemon
- INFO updated: RT2600ac + RT6600ax compatibility noted
- postinst now runs wizard --auto for first-time setup

GL kit-connect (OpenWrt ipk):
- Makefile — DEPENDS: +curl +openssh-client +tailscale
- procd init — USE_PROCD=1, respawn with 5s delay
- connect-daemon.sh — Tailscale + reverse SSH (same as Synology version)
- connect-wizard.sh — one-button setup, idempotent, UCI config
- connect-register.sh — standalone hub registration
- www/kitconnect/index.html — dark-themed dashboard with wizard button
- www/kitconnect/wizard.cgi — web-triggered wizard endpoint
- UCI config at /etc/config/kitconnect

Both platforms share: same hub protocol, same Tailscale key, same port pool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 03:10:32 +00:00
parent 0c68fb2461
commit 52e7e478e4
16 changed files with 1241 additions and 77 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
package="kit-connect"
version="0.1-0001"
description="Unified management connectivity for Pioneer bus fleet routers. Single SPK — deploys Tailscale + reverse SSH tunnel. Hub auto-assigns ports on install. One package, all routers."
description="Unified management connectivity for Pioneer bus fleet routers. Single SPK — deploys Tailscale + reverse SSH tunnel. Hub auto-assigns ports on install. One package, all routers. Compatible: RT2600ac, RT6600ax."
displayname="KIT Bus Router Connect"
maintainer="Keylink IT"
arch="ipq806x"
firmware="1.3.1-9346"
checkport="no"
startable="yes"
displayname="KIT Bus Router Connect"
thirdparty="yes"
+82
View File
@@ -0,0 +1,82 @@
#!/bin/sh
# serve-dashboard.sh — Lightweight HTTP server for the kit-connect dashboard.
# Uses busybox httpd, serves on port 8089.
# Started by start-stop-status alongside the main daemon.
PKG_DIR="/var/packages/kit-connect/target"
WWW_DIR="${PKG_DIR}/www"
CGI_DIR="${PKG_DIR}/www/cgi-bin"
PID_FILE="${PKG_DIR}/var/dashboard.pid"
PORT="${1:-8089}"
mkdir -p "$CGI_DIR" "$PKG_DIR/var"
# ── Write busybox httpd config ──────────────────────────────────
cat > "$PKG_DIR/var/httpd.conf" <<EOF
# kit-connect dashboard httpd config
*.html:text/html
*.css:text/css
*.js:text/javascript
*.json:application/json
EOF
# ── Link CGI scripts to www/cgi-bin/ ───────────────────────────
# Status endpoint
cat > "$CGI_DIR/kitconnect-status" << 'CGIEOF'
#!/bin/sh
printf "Content-Type: application/json\r\n\r\n"
DEVICE_ID="unknown"
DAEMON_RUNNING="false"
TUNNEL_PORT="0"
TAILSCALE_IP=""
HUB_REACHABLE="false"
# Device detection
DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || hostname 2>/dev/null || echo "unknown")
# Daemon check
[ -f /var/packages/kit-connect/target/var/daemon.pid ] && \
kill -0 "$(cat /var/packages/kit-connect/target/var/daemon.pid)" 2>/dev/null && \
DAEMON_RUNNING="true"
# Tunnel port from config
TUNNEL_PORT=$(grep '^TUNNEL_REMOTE_PORT=' /etc/kit-connect/connect.conf 2>/dev/null | cut -d= -f2 || echo "0")
# Tailscale
TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "")
# Hub check
HUB_HOST=$(grep '^HUB_HOST=' /etc/kit-connect/connect.conf 2>/dev/null | cut -d= -f2 || echo "162.243.83.36")
HUB_PORT=$(grep '^HUB_PORT=' /etc/kit-connect/connect.conf 2>/dev/null | cut -d= -f2 || echo "8080")
timeout 3 bash -c "echo >/dev/tcp/${HUB_HOST}/${HUB_PORT}" 2>/dev/null && HUB_REACHABLE="true"
printf '{"device_id":"%s","daemon_running":%s,"tunnel_port":"%s","tailscale_ip":"%s","hub_host":"%s","hub_port":"%s","hub_reachable":%s,"config_path":"%s","log_path":"%s"}\n' \
"$DEVICE_ID" "$DAEMON_RUNNING" "$TUNNEL_PORT" "$TAILSCALE_IP" \
"$HUB_HOST" "$HUB_PORT" "$HUB_REACHABLE" \
"/etc/kit-connect/connect.conf" \
"/var/packages/kit-connect/target/var/connect.log"
CGIEOF
chmod +x "$CGI_DIR/kitconnect-status"
# Wizard endpoint
cat > "$CGI_DIR/kitconnect-wizard" << 'CGIEOF'
#!/bin/sh
printf "Content-Type: text/plain\r\n\r\n"
/var/packages/kit-connect/target/wizard.sh --force 2>&1
CGIEOF
chmod +x "$CGI_DIR/kitconnect-wizard"
# ── Start busybox httpd ────────────────────────────────────────
if command -v busybox >/dev/null 2>&1; then
# Kill any existing instance
[ -f "$PID_FILE" ] && kill "$(cat "$PID_FILE")" 2>/dev/null
rm -f "$PID_FILE"
busybox httpd -p "$PORT" -h "$WWW_DIR" -c "$PKG_DIR/var/httpd.conf" -v 2>/dev/null &
echo $! > "$PID_FILE"
echo "Dashboard started on port ${PORT}"
else
echo "busybox not available — dashboard not started"
exit 1
fi
+24 -38
View File
@@ -1,22 +1,17 @@
#!/bin/sh
# build.sh — Assemble kit-connect SPK for Synology SRM (ipq806x)
# Compatible: RT2600ac, RT6600ax, all ipq806x SRM routers.
# Output: kit-connect-0.1-0001.spk
#
# SPK structure (outer tar, UNCOMPRESSED):
# INFO
# PACKAGE_ICON.PNG (64x64)
# PACKAGE_ICON.PNG (72x72)
# PACKAGE_ICON_256.PNG (256x256)
# scripts/
# postinst
# start-stop-status
# preinst
# preuninst
# postuninst
# postinst, start-stop-status, preinst, preuninst, postuninst
# conf/
# privilege
# resource
# package.tgz (gzipped inner tar of bin/ + conf/)
# checksum (md5 of package.tgz)
# privilege, resource
# package.tgz (gzipped inner tar: bin/ + conf/connect.conf + wizard.sh + www/)
set -e
@@ -30,42 +25,34 @@ BUILD_DIR="/tmp/kit-connect-build-$$"
echo "=== Building ${PKG_NAME}-${VERSION}.spk ==="
# Clean and create build staging directory
rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR"
# ── 1. Create inner package.tgz (gzipped tar of bin/ + conf/) ─────────
# ── 1. Create inner package.tgz ─────────────────────────────────
echo " → Creating package.tgz"
tar -czf "$BUILD_DIR/package.tgz" \
--format=ustar \
bin/ \
conf/connect.conf
--format=ustar \
bin/ \
conf/connect.conf \
wizard.sh \
www/
# ── 2. Generate checksum ──────────────────────────────────────────────
echo " → Computing checksum"
md5sum "$BUILD_DIR/package.tgz" | awk '{print $1}' > "$BUILD_DIR/checksum"
echo " → package.tgz contents:"
tar tzf "$BUILD_DIR/package.tgz"
# ── 2. Stage and assemble outer SPK (NO checksum — deprecated) ──
STAGE="$BUILD_DIR/stage"
mkdir -p "$STAGE"
cp INFO PACKAGE_ICON.PNG PACKAGE_ICON_256.PNG "$STAGE/"
cp -r scripts "$STAGE/"
mkdir -p "$STAGE/conf"
cp conf/privilege conf/resource "$STAGE/conf/"
cp "$BUILD_DIR/package.tgz" "$STAGE/"
# ── 3. Assemble outer SPK (UNCOMPRESSED tar) ──────────────────────────
echo " → Assembling SPK"
tar -cf "${OUTPUT}" \
--format=ustar \
-C "$SCRIPT_DIR" \
INFO \
PACKAGE_ICON.PNG \
PACKAGE_ICON_256.PNG \
scripts/ \
-C "$BUILD_DIR" \
package.tgz \
checksum
tar -cf "${OUTPUT}" -C "$STAGE" --format=ustar \
INFO PACKAGE_ICON.PNG PACKAGE_ICON_256.PNG scripts conf package.tgz
# SPK conf/ goes in the outer tar too
tar -rf "${OUTPUT}" \
--format=ustar \
-C "$SCRIPT_DIR" \
conf/privilege \
conf/resource
# ── 4. Verify ─────────────────────────────────────────────────────────
echo " → Verifying SPK contents"
tar -tf "${OUTPUT}" | sort
@@ -75,5 +62,4 @@ echo "=== Build complete: ${OUTPUT} ==="
echo " Size: ${SIZE} bytes"
echo " MD5: $(md5sum "${OUTPUT}" | awk '{print $1}')"
# Cleanup
rm -rf "$BUILD_DIR"
+26 -30
View File
@@ -1,43 +1,39 @@
#!/bin/sh
# kit-connect postinst — register with hub, set up config
# kit-connect postinst — run setup wizard, register with hub, set up config.
# Idempotent — safe to run on upgrades too.
PKG_DIR="/var/packages/kit-connect/target"
CONF="/etc/kit-connect/connect.conf"
LOG_TAG="kit-connect"
log() { logger -t "$LOG_TAG" -p local0.warn "$*"; }
mkdir -p /etc/kit-connect "$PKG_DIR/var"
mkdir -p /etc/kit-connect /etc/busrouter "$PKG_DIR/var"
# If config doesn't exist, create from template
if [ ! -f "$CONF" ]; then
cp "$PKG_DIR/conf/connect.conf" "$CONF"
fi
# Make scripts and wizard executable
chmod +x "$PKG_DIR/wizard.sh" 2>/dev/null || true
chmod +x "$PKG_DIR/bin/serve-dashboard.sh" 2>/dev/null || true
chmod 600 "$PKG_DIR/bin/connect_id_ed25519" 2>/dev/null || true
# Prompt for device ID if not set
DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || echo "")
if [ -z "$DEVICE_ID" ]; then
DEVICE_ID=$(hostname 2>/dev/null || echo "unknown")
fi
# Run the Keylink IT setup wizard (non-interactive)
# This detects device ID, writes config, registers with hub, starts Tailscale.
if [ -x "$PKG_DIR/wizard.sh" ]; then
log "Running Keylink IT fleet setup wizard..."
"$PKG_DIR/wizard.sh" --auto 2>&1 | while read -r line; do log "$line"; done
else
# Fallback: minimal config
DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || hostname 2>/dev/null || echo "unknown")
echo "$DEVICE_ID" > /etc/busrouter/device-id
# Write device ID into config
sed -i "s/^DEVICE_ID=.*/DEVICE_ID=$DEVICE_ID/" "$CONF" 2>/dev/null
# Set hostname for Tailscale
sed -i "s/^TAILSCALE_HOSTNAME=.*/TAILSCALE_HOSTNAME=$DEVICE_ID/" "$CONF" 2>/dev/null
# Install tailscale binaries if not present
if [ ! -f /usr/local/bin/tailscale ]; then
if [ -f "$PKG_DIR/bin/tailscale" ]; then
cp "$PKG_DIR/bin/tailscale" "$PKG_DIR/bin/tailscaled" /usr/local/bin/ 2>/dev/null
chmod +x /usr/local/bin/tailscale /usr/local/bin/tailscaled 2>/dev/null
CONF="/etc/kit-connect/connect.conf"
if [ ! -f "$CONF" ]; then
cp "$PKG_DIR/conf/connect.conf" "$CONF" 2>/dev/null || touch "$CONF"
fi
sed -i "s/^DEVICE_ID=.*/DEVICE_ID=${DEVICE_ID}/" "$CONF" 2>/dev/null
sed -i "s/^TAILSCALE_HOSTNAME=.*/TAILSCALE_HOSTNAME=${DEVICE_ID}/" "$CONF" 2>/dev/null
# Try hub registration
curl -s --connect-timeout 10 "http://162.243.83.36:8080/api/register/${DEVICE_ID}" 2>/dev/null || true
fi
# Set key permissions
chmod 600 "$PKG_DIR/bin/connect_id_ed25519" 2>/dev/null
# Try hub registration (non-fatal — daemon retries)
curl -s --connect-timeout 10 "http://162.243.83.36:8080/api/register/${DEVICE_ID}" 2>/dev/null || true
log "postinst complete — device=${DEVICE_ID}"
log "postinst complete — device_id=$(cat /etc/busrouter/device-id 2>/dev/null || echo unknown)"
exit 0
+42 -6
View File
@@ -4,28 +4,64 @@ PKG="kit-connect"
PKG_DIR="/var/packages/${PKG}/target"
DAEMON="${PKG_DIR}/bin/connect-daemon.sh"
DAEMON_PID="${PKG_DIR}/var/daemon.pid"
DASHBOARD="${PKG_DIR}/bin/serve-dashboard.sh"
DASHBOARD_PID="${PKG_DIR}/var/dashboard.pid"
case "$1" in
start)
mkdir -p "${PKG_DIR}/var"
# Start main daemon
if [ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null; then
echo "Already running"
exit 0
echo "Daemon already running"
else
setsid "$DAEMON" &
echo $! > "$DAEMON_PID"
echo "Daemon started"
fi
# Start dashboard server (port 8089)
if [ -x "$DASHBOARD" ] && command -v busybox >/dev/null 2>&1; then
if [ -f "$DASHBOARD_PID" ] && kill -0 "$(cat "$DASHBOARD_PID")" 2>/dev/null; then
echo "Dashboard already running"
else
"$DASHBOARD" 8089 &
echo $! > "$DASHBOARD_PID"
echo "Dashboard started on port 8089"
fi
fi
setsid "$DAEMON" &
echo $! > "$DAEMON_PID"
echo "Started"
;;
stop)
# Stop dashboard
if [ -f "$DASHBOARD_PID" ]; then
kill "$(cat "$DASHBOARD_PID")" 2>/dev/null
sleep 1
kill -9 "$(cat "$DASHBOARD_PID")" 2>/dev/null
rm -f "$DASHBOARD_PID"
echo "Dashboard stopped"
fi
# Stop daemon
if [ -f "$DAEMON_PID" ]; then
kill "$(cat "$DAEMON_PID")" 2>/dev/null
sleep 1
kill -9 "$(cat "$DAEMON_PID")" 2>/dev/null
rm -f "$DAEMON_PID"
echo "Daemon stopped"
fi
echo "Stopped"
;;
status)
if [ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null; then
echo "Daemon: running (PID $(cat "$DAEMON_PID"))"
else
echo "Daemon: stopped"
fi
if [ -f "$DASHBOARD_PID" ] && kill -0 "$(cat "$DASHBOARD_PID")" 2>/dev/null; then
echo "Dashboard: running on port 8089 (PID $(cat "$DASHBOARD_PID"))"
exit 0
else
echo "Dashboard: stopped"
fi
[ -f "$DAEMON_PID" ] && kill -0 "$(cat "$DAEMON_PID")" 2>/dev/null && exit 0
exit 1
;;
+137
View File
@@ -0,0 +1,137 @@
#!/bin/sh
# wizard.sh — One-button Keylink IT fleet setup for Synology routers.
# Idempotent — safe to run multiple times.
# Usage: wizard.sh [--auto] [--force]
# --auto Non-interactive (for postinst / web trigger)
# --force Re-register even if already assigned
#
# Compatible: RT2600ac, RT6600ax (all ipq806x SRM routers)
set -e
HUB_HOST="162.243.83.36"
HUB_PORT="8080"
CONF="/etc/kit-connect/connect.conf"
AUTO=0
FORCE=0
[ "$1" = "--auto" ] && AUTO=1
[ "$1" = "--force" ] || [ "$2" = "--force" ] && FORCE=1
banner() {
echo ""
echo " ╔══════════════════════════════════════════╗"
echo " ║ KEYLINK IT — Pioneer Bus Fleet Setup ║"
echo " ╚══════════════════════════════════════════╝"
echo ""
}
step() { echo "$*"; }
ok() { echo "$*"; }
warn() { echo "$*"; }
banner
# ── Step 1: Detect device ──────────────────────────────────────
step "Detecting device..."
DEVICE_ID=$(cat /etc/busrouter/device-id 2>/dev/null || true)
if [ -z "$DEVICE_ID" ]; then
DEVICE_ID=$(hostname 2>/dev/null | sed 's/[^a-zA-Z0-9_-]//g' || echo "unknown")
fi
mkdir -p /etc/busrouter
echo "$DEVICE_ID" > /etc/busrouter/device-id
ok "Device ID: ${DEVICE_ID}"
# ── Step 2: Check prerequisites ────────────────────────────────
step "Checking prerequisites..."
for cmd in curl ssh ssh-keygen; do
command -v "$cmd" >/dev/null 2>&1 || { echo " ✗ Missing: $cmd"; exit 1; }
done
ok "All prerequisites found (curl, ssh)"
# ── Step 3: Config file ────────────────────────────────────────
step "Configuring Keylink IT defaults..."
mkdir -p /etc/kit-connect /var/packages/kit-connect/target/var
if [ ! -f "$CONF" ]; then
# Create from template if available
if [ -f /var/packages/kit-connect/target/conf/connect.conf ]; then
cp /var/packages/kit-connect/target/conf/connect.conf "$CONF"
else
touch "$CONF"
fi
fi
# Write Keylink IT defaults
_write() {
grep -q "^${1}=" "$CONF" 2>/dev/null && sed -i "s|^${1}=.*|${1}=${2}|" "$CONF" || echo "${1}=${2}" >> "$CONF"
}
_write "DEVICE_ID" "$DEVICE_ID"
_write "HUB_HOST" "$HUB_HOST"
_write "HUB_PORT" "$HUB_PORT"
_write "TUNNEL_ENABLE" "1"
_write "TUNNEL_REMOTE_HOST" "$HUB_HOST"
_write "TUNNEL_REMOTE_USER" "node"
_write "TUNNEL_REMOTE_SSH_PORT" "22"
_write "TUNNEL_REMOTE_PORT" "0" # auto-assign
_write "TUNNEL_LOCAL_SSH_PORT" "2223"
_write "TUNNEL_RETRY_DELAY" "30"
_write "TAILSCALE_ENABLE" "1"
_write "TAILSCALE_AUTH_KEY" "tskey-auth-kJz8wqNVo211CNTRL-GNL5EFjp5aWQcaWPSVn2aW9TNworKUNBV"
_write "TAILSCALE_HOSTNAME" "$DEVICE_ID"
_write "WATCHDOG_INTERVAL" "60"
_write "FORWARD_FAIL_LIMIT" "2"
ok "Config written to ${CONF}"
# ── Step 4: Register with hub ──────────────────────────────────
step "Registering with fleet hub (${HUB_HOST}:${HUB_PORT})..."
resp=$(curl -s --connect-timeout 10 "http://${HUB_HOST}:${HUB_PORT}/api/register/${DEVICE_ID}" 2>/dev/null) || true
if [ -z "$resp" ]; then
warn "Hub unreachable — will retry on next daemon start"
ok "Config saved locally (port will auto-assign when hub is reachable)"
else
port=$(echo "$resp" | grep -o '"tunnel_port"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*')
status=$(echo "$resp" | grep -o '"status"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
tskey=$(echo "$resp" | grep -o '"tailscale_auth_key"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4)
if [ -n "$port" ] && [ "$port" != "0" ]; then
sed -i "s/^TUNNEL_REMOTE_PORT=.*/TUNNEL_REMOTE_PORT=${port}/" "$CONF" 2>/dev/null
ok "Hub assigned port ${port} (status: ${status:-ok})"
echo "$port" > /var/packages/kit-connect/target/var/hub_port
fi
if [ -n "$tskey" ]; then
sed -i "s|^TAILSCALE_AUTH_KEY=.*|TAILSCALE_AUTH_KEY=${tskey}|" "$CONF" 2>/dev/null
ok "Tailscale key configured"
fi
fi
# ── Step 5: Dashboard server ───────────────────────────────────
step "Setting up local dashboard..."
mkdir -p /var/packages/kit-connect/target/www
# Dashboard is served by busybox httpd on port 8089
ok "Dashboard will be available on port 8089"
# ── Step 6: Start services ─────────────────────────────────────
step "Starting kit-connect service..."
if [ -x /var/packages/kit-connect/scripts/start-stop-status ]; then
/var/packages/kit-connect/scripts/start-stop-status start 2>/dev/null && \
ok "Service started" || warn "Service start returned non-zero"
fi
# ── Done ───────────────────────────────────────────────────────
ASSIGNED=$(grep '^TUNNEL_REMOTE_PORT=' "$CONF" 2>/dev/null | cut -d= -f2 || echo "pending")
echo ""
echo " ╔══════════════════════════════════════════╗"
echo " ║ SETUP COMPLETE ║"
echo " ╠══════════════════════════════════════════╣"
printf " ║ Device: %-30s ║\n" "$DEVICE_ID"
printf " ║ Port: %-30s ║\n" "$ASSIGNED"
printf " ║ Hub: %-30s ║\n" "${HUB_HOST}:${HUB_PORT}"
echo " ╠══════════════════════════════════════════╣"
echo " ║ Dashboard: http://${DEVICE_ID}:8089/ ║"
echo " ╚══════════════════════════════════════════╝"
echo ""
exit 0
+136
View File
@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kit Connect — Fleet Status</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0d1117; color: #e6edf3; min-height: 100vh; }
.header { background: linear-gradient(135deg, #2563EB, #1d4ed8); padding: 16px 24px;
display: flex; align-items: center; gap: 14px; border-bottom: 2px solid #60a5fa; }
.logo { font-size: 22px; font-weight: 700; color: #fff; }
.logo span { color: #93c5fd; }
.sub { font-size: 12px; color: #bfdbfe; letter-spacing: 2px; text-transform: uppercase; }
.refresh { margin-left: auto; font-size: 11px; color: #93c5fd; }
.container { max-width: 800px; margin: 0 auto; padding: 20px 16px; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px;
padding: 20px; margin-bottom: 16px; }
.card-title { font-size: 11px; text-transform: uppercase; letter-spacing: 1px;
color: #8b949e; margin-bottom: 14px; }
.badge { display: inline-block; padding: 2px 10px; border-radius: 12px;
font-size: 11px; font-weight: 700; }
.badge-ok { background: #1a4a2e; color: #3fb950; border: 1px solid #3fb950; }
.badge-warn { background: #3d2b00; color: #d29922; border: 1px solid #d29922; }
.badge-err { background: #4a1a1a; color: #f85149; border: 1px solid #f85149; }
.row { display: flex; justify-content: space-between; align-items: center;
padding: 8px 0; border-bottom: 1px solid #21262d; }
.row:last-child { border: none; }
.label { color: #8b949e; font-size: 14px; }
.value { font-weight: 600; font-size: 14px; font-family: "SF Mono", "Consolas", monospace; }
.btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 24px;
border-radius: 6px; border: none; cursor: pointer; font-size: 14px;
font-weight: 600; transition: all 0.2s; }
.btn-wizard { background: linear-gradient(135deg, #2563EB, #1d4ed8); color: #fff;
width: 100%; justify-content: center; font-size: 15px; padding: 14px; }
.btn-wizard:hover { background: linear-gradient(135deg, #3b82f6, #2563EB); }
.btn-wizard:disabled { opacity: 0.5; cursor: not-allowed; }
.output { margin-top: 12px; padding: 12px; background: #0d1117; border-radius: 5px;
font-family: "SF Mono", "Consolas", monospace; font-size: 12px;
white-space: pre-wrap; color: #3fb950; display: none; max-height: 300px; overflow-y: auto; }
.footer { text-align: center; padding: 12px; font-size: 11px; color: #484f58;
border-top: 1px solid #21262d; margin-top: 20px; }
</style>
</head>
<body>
<div class="header">
<div>
<div class="logo">KIT <span>CONNECT</span></div>
<div class="sub">Keylink IT Fleet Connectivity</div>
</div>
<div class="refresh" id="ts">Loading…</div>
</div>
<div class="container">
<div class="card">
<div class="card-title">⚡ One-Click Setup</div>
<p style="font-size:13px;color:#8b949e;margin-bottom:12px">
Configures Keylink IT fleet defaults — device ID, hub, Tailscale, reverse SSH.
Safe to run multiple times.
</p>
<button class="btn btn-wizard" id="wiz-btn" onclick="runWizard()">
🔧 Run Setup Wizard
</button>
<div class="output" id="wiz-out"></div>
</div>
<div class="card">
<div class="card-title">📡 Connectivity Status</div>
<div id="status-body"><div class="row"><span class="label">Loading…</span></div></div>
</div>
<div class="footer">
kit-connect v0.1-0001 · device: <span id="dev-id">-</span> ·
<span id="footer-ts"></span>
</div>
</div>
<script>
var ESC = function(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); };
function row(label, value) {
return '<div class="row"><span class="label">' + ESC(label) + '</span>' +
'<span class="value">' + ESC(value) + '</span></div>';
}
function badge(label, type) {
return '<span class="badge badge-' + (type||'ok') + '">' + ESC(label) + '</span>';
}
function poll() {
document.getElementById('ts').textContent = 'Updated ' + new Date().toLocaleTimeString();
document.getElementById('footer-ts').textContent = new Date().toLocaleTimeString();
// Check daemon, tunnel, tailscale via status endpoint
fetch('/cgi-bin/kitconnect-status')
.then(function(r) { return r.ok ? r.json() : Promise.reject(r.status); })
.then(function(d) {
var html = '';
html += row('Device', d.device_id || '-');
html += row('Daemon', (d.daemon_running ? badge('RUNNING','ok') : badge('STOPPED','err')));
html += row('Tunnel', d.tunnel_port ? ('port ' + d.tunnel_port + ' ' + badge('ACTIVE','ok')) : badge('PENDING','warn'));
html += row('Tailscale', d.tailscale_ip ? (d.tailscale_ip + ' ' + badge('CONNECTED','ok')) : badge('NOT CONNECTED','warn'));
html += row('Hub', d.hub_host + ':' + d.hub_port + ' ' + badge(d.hub_reachable ? 'REACHABLE':'UNREACHABLE', d.hub_reachable ? 'ok' : 'warn'));
html += row('Config', d.config_path);
html += row('Log', d.log_path);
document.getElementById('status-body').innerHTML = html;
document.getElementById('dev-id').textContent = d.device_id || '-';
})
.catch(function(e) {
document.getElementById('status-body').innerHTML =
'<div class="row"><span class="label" style="color:#f85149">Dashboard unavailable (' + ESC(String(e)) + ')</span></div>';
});
}
function runWizard() {
var btn = document.getElementById('wiz-btn');
var out = document.getElementById('wiz-out');
btn.disabled = true; btn.textContent = '⏳ Running…';
out.style.display = 'block'; out.textContent = 'Starting wizard...\n';
fetch('/cgi-bin/kitconnect-wizard', { method: 'POST' })
.then(function(r) { return r.text(); })
.then(function(t) {
out.textContent = t;
out.style.color = t.includes('COMPLETE') ? '#3fb950' : '#d29922';
})
.catch(function(e) { out.textContent = 'ERROR: ' + e; out.style.color = '#f85149'; })
.finally(function() { btn.disabled = false; btn.textContent = '🔧 Run Setup Wizard'; setTimeout(poll, 2000); });
}
poll();
setInterval(poll, 30000);
</script>
</body>
</html>