Add offline license capture to setup
This commit is contained in:
parent
9ce9efc82e
commit
f407a9331e
@ -34,6 +34,7 @@ DATA_DIR = Path(os.environ.get("CEZEN_DATA", "/opt/cezen/data"))
|
|||||||
DB_PATH = DATA_DIR / "cezen.db"
|
DB_PATH = DATA_DIR / "cezen.db"
|
||||||
SECRET_FILE = DATA_DIR / ".jwt_secret"
|
SECRET_FILE = DATA_DIR / ".jwt_secret"
|
||||||
BACKUP_DIR = Path(os.environ.get("CEZEN_BACKUP_DIR", str(DATA_DIR.parent / "backups")))
|
BACKUP_DIR = Path(os.environ.get("CEZEN_BACKUP_DIR", str(DATA_DIR.parent / "backups")))
|
||||||
|
LICENSE_FILE = Path(os.environ.get("CEZEN_LICENSE_JSON", "/opt/cezen/license.json"))
|
||||||
|
|
||||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
@ -825,22 +826,46 @@ def _normalize_tier(value: str) -> str:
|
|||||||
return raw
|
return raw
|
||||||
return TIER_ALIASES.get(raw, "basic")
|
return TIER_ALIASES.get(raw, "basic")
|
||||||
|
|
||||||
|
def _license_record() -> dict:
|
||||||
|
try:
|
||||||
|
if LICENSE_FILE.exists():
|
||||||
|
data = json.loads(LICENSE_FILE.read_text())
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _public_license_record(record: dict) -> dict:
|
||||||
|
public = {k: v for k, v in (record or {}).items() if k != "license_key"}
|
||||||
|
if record.get("license_key"):
|
||||||
|
public["license_key_prefix"] = record["license_key"][:12]
|
||||||
|
public["license_key_present"] = True
|
||||||
|
else:
|
||||||
|
public["license_key_present"] = False
|
||||||
|
return public
|
||||||
|
|
||||||
def _current_tier() -> str:
|
def _current_tier() -> str:
|
||||||
if CEZEN_TIER:
|
if CEZEN_TIER:
|
||||||
return _normalize_tier(CEZEN_TIER)
|
return _normalize_tier(CEZEN_TIER)
|
||||||
|
license_tier = (_license_record().get("tier") or "").strip()
|
||||||
|
if license_tier:
|
||||||
|
return _normalize_tier(license_tier)
|
||||||
return _normalize_tier(_setting_value("tier_label", "Basic"))
|
return _normalize_tier(_setting_value("tier_label", "Basic"))
|
||||||
|
|
||||||
def _tier_payload() -> dict:
|
def _tier_payload() -> dict:
|
||||||
tier = _current_tier()
|
tier = _current_tier()
|
||||||
info = TIER_MATRIX[tier]
|
info = TIER_MATRIX[tier]
|
||||||
|
license_record = _license_record()
|
||||||
return {
|
return {
|
||||||
"tier": tier,
|
"tier": tier,
|
||||||
"label": info["label"],
|
"label": info["label"],
|
||||||
"locked": bool(CEZEN_TIER),
|
"locked": bool(CEZEN_TIER or license_record.get("tier")),
|
||||||
"positioning": info["positioning"],
|
"positioning": info["positioning"],
|
||||||
"max_users": info["max_users"],
|
"max_users": info["max_users"],
|
||||||
"features": info["features"],
|
"features": info["features"],
|
||||||
"tiers": TIER_MATRIX,
|
"tiers": TIER_MATRIX,
|
||||||
|
"license_record": _public_license_record(license_record),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _readiness_score(feasibility: dict, license_info: dict) -> dict:
|
def _readiness_score(feasibility: dict, license_info: dict) -> dict:
|
||||||
@ -1777,12 +1802,20 @@ async def get_branding():
|
|||||||
rows = db.execute("SELECT key, value FROM settings").fetchall()
|
rows = db.execute("SELECT key, value FROM settings").fetchall()
|
||||||
db.close()
|
db.close()
|
||||||
result = {r["key"]: r["value"] for r in rows}
|
result = {r["key"]: r["value"] for r in rows}
|
||||||
|
license_record = _license_record()
|
||||||
# If Cezen has locked the tier via env var, override whatever is in DB
|
# If Cezen has locked the tier via env var, override whatever is in DB
|
||||||
if CEZEN_TIER:
|
if CEZEN_TIER or license_record.get("tier"):
|
||||||
result["tier_label"] = CEZEN_TIER
|
tier = _current_tier()
|
||||||
|
result["tier_label"] = TIER_MATRIX[tier]["label"]
|
||||||
|
result["tier_slug"] = tier
|
||||||
result["tier_locked"] = "true"
|
result["tier_locked"] = "true"
|
||||||
else:
|
else:
|
||||||
result["tier_locked"] = "false"
|
result["tier_locked"] = "false"
|
||||||
|
if license_record:
|
||||||
|
result["license_customer"] = license_record.get("customer_name", "")
|
||||||
|
result["license_customer_id"] = license_record.get("customer_id", "")
|
||||||
|
result["license_key_prefix"] = (license_record.get("license_key", "") or "")[:12]
|
||||||
|
result["license_support_until"] = license_record.get("support_until", "")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@app.put("/api/settings/branding")
|
@app.put("/api/settings/branding")
|
||||||
|
|||||||
@ -30,6 +30,19 @@
|
|||||||
group: "{{ cezen_user }}"
|
group: "{{ cezen_user }}"
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
|
|
||||||
|
- name: Check for captured license file
|
||||||
|
stat:
|
||||||
|
path: /opt/cezen/license.json
|
||||||
|
register: cezen_license_file
|
||||||
|
|
||||||
|
- name: Allow backend service to read captured license
|
||||||
|
file:
|
||||||
|
path: /opt/cezen/license.json
|
||||||
|
owner: root
|
||||||
|
group: "{{ cezen_user }}"
|
||||||
|
mode: "0640"
|
||||||
|
when: cezen_license_file.stat.exists
|
||||||
|
|
||||||
- name: Copy FastAPI application
|
- name: Copy FastAPI application
|
||||||
copy:
|
copy:
|
||||||
src: main.py
|
src: main.py
|
||||||
|
|||||||
@ -42,7 +42,7 @@ W=70
|
|||||||
|
|
||||||
# ── Welcome ────────────────────────────────────────────────
|
# ── Welcome ────────────────────────────────────────────────
|
||||||
whiptail --title "$TITLE" \
|
whiptail --title "$TITLE" \
|
||||||
--msgbox "\nWelcome to the Nexus One AI installer.\n\nThis wizard will configure your network and install the AI stack.\n\nMake sure this server is connected to the internet before continuing." \
|
--msgbox "\nWelcome to the Nexus One AI installer.\n\nThis wizard will configure your network, capture license/customer details, and install the AI stack.\n\nMake sure this server is connected to the internet before continuing." \
|
||||||
$H $W
|
$H $W
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
@ -50,7 +50,7 @@ whiptail --title "$TITLE" \
|
|||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
NET_MODE=$(whiptail --title "$TITLE" \
|
NET_MODE=$(whiptail --title "$TITLE" \
|
||||||
--menu "\nStep 1 of 3: Network Configuration\n\nHow should this server get its IP address?" \
|
--menu "\nStep 1 of 4: Network Configuration\n\nHow should this server get its IP address?" \
|
||||||
$H $W 2 \
|
$H $W 2 \
|
||||||
"DHCP" "Automatic (get IP from network)" \
|
"DHCP" "Automatic (get IP from network)" \
|
||||||
"Static" "Manual (enter IP address)" \
|
"Static" "Manual (enter IP address)" \
|
||||||
@ -131,11 +131,35 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# STEP 2: SELECT TIER
|
# STEP 2: LICENSE / CUSTOMER DETAILS
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CUSTOMER_NAME=$(whiptail --title "$TITLE" \
|
||||||
|
--inputbox "\nStep 2 of 4: License & Customer Details\n\nCustomer / organisation name:" \
|
||||||
|
$H $W "" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
CUSTOMER_ID=$(whiptail --title "$TITLE" \
|
||||||
|
--inputbox "\nStep 2 of 4: License & Customer Details\n\nCustomer ID / project code:\n\nLeave blank for evaluation or field staging." \
|
||||||
|
$H $W "" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
CONTACT_EMAIL=$(whiptail --title "$TITLE" \
|
||||||
|
--inputbox "\nStep 2 of 4: License & Customer Details\n\nCustomer/admin contact email:\n\nLeave blank if unavailable on site." \
|
||||||
|
$H $W "" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
LICENSE_KEY=$(whiptail --title "$TITLE" \
|
||||||
|
--passwordbox "\nStep 2 of 4: License & Customer Details\n\nLicense key / activation code:\n\nLeave blank for offline field staging. Models and keys can be added later." \
|
||||||
|
$H $W "" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
SUPPORT_UNTIL=$(whiptail --title "$TITLE" \
|
||||||
|
--inputbox "\nStep 2 of 4: License & Customer Details\n\nSupport valid until (YYYY-MM-DD):\n\nLeave blank if not issued yet." \
|
||||||
|
$H $W "" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
|
# ════════════════════════════════════════════════════════════
|
||||||
|
# STEP 3: SELECT TIER
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
TIER=$(whiptail --title "$TITLE" \
|
TIER=$(whiptail --title "$TITLE" \
|
||||||
--menu "\nStep 2 of 3: Select AI Package Tier\n\nChoose the tier that matches your hardware:" \
|
--menu "\nStep 3 of 4: Select AI Package Tier\n\nChoose the tier that matches your hardware:" \
|
||||||
$H $W 4 \
|
$H $W 4 \
|
||||||
"starter" "Starter — 1× RTX 5090 / 32GB VRAM · Small team" \
|
"starter" "Starter — 1× RTX 5090 / 32GB VRAM · Small team" \
|
||||||
"basic" "Entry — 1× NVIDIA RTX Pro 6000 (96GB) · Up to 20 users" \
|
"basic" "Entry — 1× NVIDIA RTX Pro 6000 (96GB) · Up to 20 users" \
|
||||||
@ -144,11 +168,11 @@ TIER=$(whiptail --title "$TITLE" \
|
|||||||
3>&1 1>&2 2>&3)
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# STEP 3: SELECT AI TOOLS
|
# STEP 4: SELECT AI TOOLS
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
TOOLS=$(whiptail --title "$TITLE" \
|
TOOLS=$(whiptail --title "$TITLE" \
|
||||||
--checklist "\nStep 3 of 3: Select AI Tools to Install\n\nSpace to toggle, Enter to confirm:" \
|
--checklist "\nStep 4 of 4: Select AI Tools to Install\n\nSpace to toggle, Enter to confirm:" \
|
||||||
$H $W 8 \
|
$H $W 8 \
|
||||||
"ollama" "Ollama + Open WebUI (LLM inference + chat UI)" ON \
|
"ollama" "Ollama + Open WebUI (LLM inference + chat UI)" ON \
|
||||||
"jupyterlab" "JupyterLab (Notebook environment)" ON \
|
"jupyterlab" "JupyterLab (Notebook environment)" ON \
|
||||||
@ -167,9 +191,13 @@ TOOLS=$(whiptail --title "$TITLE" \
|
|||||||
# Format tools list for display
|
# Format tools list for display
|
||||||
TOOLS_DISPLAY=$(echo "$TOOLS" | tr -d '"' | tr ' ' '\n' | sed 's/^/ · /' | tr '\n' '\n')
|
TOOLS_DISPLAY=$(echo "$TOOLS" | tr -d '"' | tr ' ' '\n' | sed 's/^/ · /' | tr '\n' '\n')
|
||||||
MY_IP=$(hostname -I | awk '{print $1}')
|
MY_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
LICENSE_DISPLAY="Field staging / evaluation"
|
||||||
|
if [ -n "$LICENSE_KEY" ]; then
|
||||||
|
LICENSE_DISPLAY="Provided"
|
||||||
|
fi
|
||||||
|
|
||||||
whiptail --title "$TITLE" \
|
whiptail --title "$TITLE" \
|
||||||
--yesno "\nReady to install. Please confirm:\n\nNetwork: ${NET_MODE} (${MY_IP})\nTier: ${TIER}\n\nTools:\n${TOOLS_DISPLAY}\n\nThis will take 20–40 minutes.\nThe server will reboot once during install (NVIDIA drivers).\n\nContinue?" \
|
--yesno "\nReady to install. Please confirm:\n\nNetwork: ${NET_MODE} (${MY_IP})\nCustomer: ${CUSTOMER_NAME:-Not entered}\nLicense: ${LICENSE_DISPLAY}\nTier: ${TIER}\n\nTools:\n${TOOLS_DISPLAY}\n\nThis will take 20–40 minutes.\nThe server will reboot once during install (NVIDIA drivers).\n\nContinue?" \
|
||||||
$H $W
|
$H $W
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
@ -202,6 +230,30 @@ TIER=${TIER}
|
|||||||
SKIP_ROLES=${SKIP_ROLES}
|
SKIP_ROLES=${SKIP_ROLES}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
export CUSTOMER_NAME CUSTOMER_ID CONTACT_EMAIL LICENSE_KEY SUPPORT_UNTIL TIER
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json, os, time
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"schema": "cezen.license.v1",
|
||||||
|
"customer_name": os.environ.get("CUSTOMER_NAME", "").strip(),
|
||||||
|
"customer_id": os.environ.get("CUSTOMER_ID", "").strip(),
|
||||||
|
"contact_email": os.environ.get("CONTACT_EMAIL", "").strip(),
|
||||||
|
"license_key": os.environ.get("LICENSE_KEY", "").strip(),
|
||||||
|
"tier": os.environ.get("TIER", "basic").strip(),
|
||||||
|
"support_until": os.environ.get("SUPPORT_UNTIL", "").strip(),
|
||||||
|
"install_type": "licensed" if os.environ.get("LICENSE_KEY", "").strip() else "field-staging",
|
||||||
|
"issued_by": "Cezen",
|
||||||
|
"captured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"signature_status": "unsigned-field-install",
|
||||||
|
}
|
||||||
|
|
||||||
|
with open("/opt/cezen/license.json", "w") as f:
|
||||||
|
json.dump(payload, f, indent=2)
|
||||||
|
PY
|
||||||
|
chown root:cezen /opt/cezen/license.json 2>/dev/null || true
|
||||||
|
chmod 0640 /opt/cezen/license.json
|
||||||
|
|
||||||
# Mark as configured so this wizard doesn't run again
|
# Mark as configured so this wizard doesn't run again
|
||||||
touch /opt/cezen/.setup-done
|
touch /opt/cezen/.setup-done
|
||||||
|
|
||||||
|
|||||||
@ -91,13 +91,35 @@ def apply_static_ip(iface, ip, prefix, gateway, dns):
|
|||||||
subprocess.run(["netplan", "apply"], capture_output=True)
|
subprocess.run(["netplan", "apply"], capture_output=True)
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
def run_install(tier, skip_tools):
|
def write_license_file(license_data, tier):
|
||||||
|
os.makedirs("/opt/cezen", exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
"schema": "cezen.license.v1",
|
||||||
|
"customer_name": (license_data.get("customer_name") or "").strip(),
|
||||||
|
"customer_id": (license_data.get("customer_id") or "").strip(),
|
||||||
|
"contact_email": (license_data.get("contact_email") or "").strip(),
|
||||||
|
"license_key": (license_data.get("license_key") or "").strip(),
|
||||||
|
"tier": tier,
|
||||||
|
"support_until": (license_data.get("support_until") or "").strip(),
|
||||||
|
"install_type": (license_data.get("install_type") or "customer").strip(),
|
||||||
|
"issued_by": "Cezen",
|
||||||
|
"captured_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"signature_status": "unsigned-field-install",
|
||||||
|
}
|
||||||
|
with open("/opt/cezen/license.json", "w") as f:
|
||||||
|
json.dump(payload, f, indent=2)
|
||||||
|
subprocess.run(["chown", "root:cezen", "/opt/cezen/license.json"], check=False)
|
||||||
|
os.chmod("/opt/cezen/license.json", 0o640)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def run_install(tier, skip_tools, license_data=None):
|
||||||
global install_status
|
global install_status
|
||||||
install_status = {"running": True, "done": False, "error": None}
|
install_status = {"running": True, "done": False, "error": None}
|
||||||
try:
|
try:
|
||||||
# Write config so phase 2 (post-reboot) knows what to skip
|
# Write config so phase 2 (post-reboot) knows what to skip
|
||||||
os.makedirs("/opt/cezen", exist_ok=True)
|
os.makedirs("/opt/cezen", exist_ok=True)
|
||||||
skip_str = ",".join(skip_tools) if skip_tools else ""
|
skip_str = ",".join(skip_tools) if skip_tools else ""
|
||||||
|
write_license_file(license_data or {}, tier)
|
||||||
with open("/opt/cezen/install.conf", "w") as f:
|
with open("/opt/cezen/install.conf", "w") as f:
|
||||||
f.write(f"TIER={tier}\nSKIP_ROLES={skip_str}\n")
|
f.write(f"TIER={tier}\nSKIP_ROLES={skip_str}\n")
|
||||||
|
|
||||||
@ -258,9 +280,10 @@ HTML = r"""<!DOCTYPE html>
|
|||||||
|
|
||||||
<div class="steps" id="step-bar">
|
<div class="steps" id="step-bar">
|
||||||
<div class="step active" id="sbar-1">1 · Network</div>
|
<div class="step active" id="sbar-1">1 · Network</div>
|
||||||
<div class="step" id="sbar-2">2 · Select Tier</div>
|
<div class="step" id="sbar-2">2 · License</div>
|
||||||
<div class="step" id="sbar-3">3 · AI Tools</div>
|
<div class="step" id="sbar-3">3 · Select Tier</div>
|
||||||
<div class="step" id="sbar-4">4 · Install</div>
|
<div class="step" id="sbar-4">4 · AI Tools</div>
|
||||||
|
<div class="step" id="sbar-5">5 · Install</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── STEP 1: NETWORK ── -->
|
<!-- ── STEP 1: NETWORK ── -->
|
||||||
@ -311,8 +334,40 @@ HTML = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── STEP 2: TIER ── -->
|
<!-- ── STEP 2: LICENSE ── -->
|
||||||
<div id="step-2" class="hidden">
|
<div id="step-2" class="hidden">
|
||||||
|
<div class="card">
|
||||||
|
<h2>License & Customer Details</h2>
|
||||||
|
<p class="desc">Enter the customer/license details for this installation. Leave the license key blank for evaluation or PSU field staging.</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Customer / Organisation Name</label>
|
||||||
|
<input type="text" id="lic-customer-name" placeholder="e.g. Bharat Electronics Limited">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Customer ID / Project Code</label>
|
||||||
|
<input type="text" id="lic-customer-id" placeholder="e.g. BEL-PSU-2026">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Contact Email</label>
|
||||||
|
<input type="email" id="lic-contact-email" placeholder="admin@example.gov.in">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>License Key / Activation Code</label>
|
||||||
|
<input type="text" id="lic-license-key" placeholder="Optional for offline field install">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Support Until</label>
|
||||||
|
<input type="date" id="lic-support-until">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn btn-secondary" onclick="goStep(1)">← Back</button>
|
||||||
|
<button class="btn btn-primary" onclick="goStep(3)">Next →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── STEP 3: TIER ── -->
|
||||||
|
<div id="step-3" class="hidden">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Select AI Package Tier</h2>
|
<h2>Select AI Package Tier</h2>
|
||||||
<p class="desc">Choose the tier that matches your GPU hardware.</p>
|
<p class="desc">Choose the tier that matches your GPU hardware.</p>
|
||||||
@ -340,13 +395,13 @@ HTML = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn btn-secondary" onclick="goStep(1)">← Back</button>
|
<button class="btn btn-secondary" onclick="goStep(2)">← Back</button>
|
||||||
<button class="btn btn-primary" onclick="goStep(3)">Next →</button>
|
<button class="btn btn-primary" onclick="goStep(4)">Next →</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── STEP 3: TOOLS ── -->
|
<!-- ── STEP 4: TOOLS ── -->
|
||||||
<div id="step-3" class="hidden">
|
<div id="step-4" class="hidden">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Select AI Tools</h2>
|
<h2>Select AI Tools</h2>
|
||||||
<p class="desc">Toggle the components you want installed. Recommended defaults are pre-selected.</p>
|
<p class="desc">Toggle the components you want installed. Recommended defaults are pre-selected.</p>
|
||||||
@ -355,13 +410,13 @@ HTML = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn btn-secondary" onclick="goStep(2)">← Back</button>
|
<button class="btn btn-secondary" onclick="goStep(3)">← Back</button>
|
||||||
<button class="btn btn-primary" onclick="goStep(4)">Review & Install →</button>
|
<button class="btn btn-primary" onclick="goStep(5)">Review & Install →</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── STEP 4: CONFIRM + INSTALL ── -->
|
<!-- ── STEP 5: CONFIRM + INSTALL ── -->
|
||||||
<div id="step-4" class="hidden">
|
<div id="step-5" class="hidden">
|
||||||
<div class="card" id="summary-card">
|
<div class="card" id="summary-card">
|
||||||
<h2>Review Configuration</h2>
|
<h2>Review Configuration</h2>
|
||||||
<p class="desc">Confirm your settings before installation begins.</p>
|
<p class="desc">Confirm your settings before installation begins.</p>
|
||||||
@ -395,7 +450,7 @@ HTML = r"""<!DOCTYPE html>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-row" id="install-btn-row">
|
<div class="btn-row" id="install-btn-row">
|
||||||
<button class="btn btn-secondary" onclick="goStep(3)">← Back</button>
|
<button class="btn btn-secondary" onclick="goStep(4)">← Back</button>
|
||||||
<button class="btn btn-primary" id="install-btn" onclick="startInstall()">🚀 Start Installation</button>
|
<button class="btn btn-primary" id="install-btn" onclick="startInstall()">🚀 Start Installation</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -428,15 +483,15 @@ window.onload = () => {
|
|||||||
|
|
||||||
// ── Navigation ─────────────────────────────────────────────
|
// ── Navigation ─────────────────────────────────────────────
|
||||||
function goStep(n) {
|
function goStep(n) {
|
||||||
if (n === 2 && !selectedTier) { alert('Please select a tier.'); return; }
|
if (n === 4 && !selectedTier) { alert('Please select a tier.'); return; }
|
||||||
[1,2,3,4].forEach(i => {
|
[1,2,3,4,5].forEach(i => {
|
||||||
document.getElementById('step-'+i).classList.toggle('hidden', i !== n);
|
document.getElementById('step-'+i).classList.toggle('hidden', i !== n);
|
||||||
const bar = document.getElementById('sbar-'+i);
|
const bar = document.getElementById('sbar-'+i);
|
||||||
bar.classList.remove('active','done');
|
bar.classList.remove('active','done');
|
||||||
if (i === n) bar.classList.add('active');
|
if (i === n) bar.classList.add('active');
|
||||||
if (i < n) bar.classList.add('done');
|
if (i < n) bar.classList.add('done');
|
||||||
});
|
});
|
||||||
if (n === 4) renderSummary();
|
if (n === 5) renderSummary();
|
||||||
if (n === 2 && netMode === 'static') applyStaticIP();
|
if (n === 2 && netMode === 'static') applyStaticIP();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,15 +550,30 @@ function toggleTool(k) {
|
|||||||
document.getElementById('tool-'+k).classList.toggle('selected', tools[k].on);
|
document.getElementById('tool-'+k).classList.toggle('selected', tools[k].on);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── License ────────────────────────────────────────────────
|
||||||
|
function collectLicense() {
|
||||||
|
return {
|
||||||
|
customer_name: document.getElementById('lic-customer-name').value.trim(),
|
||||||
|
customer_id: document.getElementById('lic-customer-id').value.trim(),
|
||||||
|
contact_email: document.getElementById('lic-contact-email').value.trim(),
|
||||||
|
license_key: document.getElementById('lic-license-key').value.trim(),
|
||||||
|
support_until: document.getElementById('lic-support-until').value.trim(),
|
||||||
|
install_type: document.getElementById('lic-license-key').value.trim() ? 'licensed' : 'field-staging'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ── Summary ────────────────────────────────────────────────
|
// ── Summary ────────────────────────────────────────────────
|
||||||
function renderSummary() {
|
function renderSummary() {
|
||||||
const ip = netMode === 'dhcp'
|
const ip = netMode === 'dhcp'
|
||||||
? document.getElementById('current-ip').textContent + ' (DHCP)'
|
? document.getElementById('current-ip').textContent + ' (DHCP)'
|
||||||
: document.getElementById('ip-addr').value + '/' + document.getElementById('ip-prefix').value;
|
: document.getElementById('ip-addr').value + '/' + document.getElementById('ip-prefix').value;
|
||||||
|
const license = collectLicense();
|
||||||
const onTools = Object.entries(tools).filter(([,v])=>v.on).map(([,v])=>v.name).join(', ');
|
const onTools = Object.entries(tools).filter(([,v])=>v.on).map(([,v])=>v.name).join(', ');
|
||||||
const offTools = Object.entries(tools).filter(([,v])=>!v.on);
|
const offTools = Object.entries(tools).filter(([,v])=>!v.on);
|
||||||
document.getElementById('summary-rows').innerHTML = `
|
document.getElementById('summary-rows').innerHTML = `
|
||||||
<div class="summary-row"><span class="key">Network</span><span class="val">${ip}</span></div>
|
<div class="summary-row"><span class="key">Network</span><span class="val">${ip}</span></div>
|
||||||
|
<div class="summary-row"><span class="key">Customer</span><span class="val">${license.customer_name || 'Not entered'}</span></div>
|
||||||
|
<div class="summary-row"><span class="key">License</span><span class="val">${license.license_key ? 'Provided' : 'Field staging / evaluation'}</span></div>
|
||||||
<div class="summary-row"><span class="key">Tier</span><span class="val">${selectedTier.charAt(0).toUpperCase()+selectedTier.slice(1)}</span></div>
|
<div class="summary-row"><span class="key">Tier</span><span class="val">${selectedTier.charAt(0).toUpperCase()+selectedTier.slice(1)}</span></div>
|
||||||
<div class="summary-row"><span class="key">Tools</span><span class="val" style="font-size:13px;text-align:right;max-width:60%">${onTools}</span></div>
|
<div class="summary-row"><span class="key">Tools</span><span class="val" style="font-size:13px;text-align:right;max-width:60%">${onTools}</span></div>
|
||||||
${offTools.length ? `<div class="summary-row"><span class="key">Skipped</span><span class="val" style="color:var(--muted);font-size:13px">${offTools.map(([,v])=>v.name).join(', ')}</span></div>` : ''}
|
${offTools.length ? `<div class="summary-row"><span class="key">Skipped</span><span class="val" style="color:var(--muted);font-size:13px">${offTools.map(([,v])=>v.name).join(', ')}</span></div>` : ''}
|
||||||
@ -520,7 +590,7 @@ function startInstall() {
|
|||||||
fetch('/api/install', {
|
fetch('/api/install', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type':'application/json'},
|
headers: {'Content-Type':'application/json'},
|
||||||
body: JSON.stringify({ tier: selectedTier, skip_tools: skip })
|
body: JSON.stringify({ tier: selectedTier, skip_tools: skip, license: collectLicense() })
|
||||||
});
|
});
|
||||||
|
|
||||||
streamLog();
|
streamLog();
|
||||||
@ -714,8 +784,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
global install_proc
|
global install_proc
|
||||||
tier = body.get("tier", "basic")
|
tier = body.get("tier", "basic")
|
||||||
skip = body.get("skip_tools", [])
|
skip = body.get("skip_tools", [])
|
||||||
|
license_data = body.get("license", {})
|
||||||
if not install_status["running"]:
|
if not install_status["running"]:
|
||||||
t = threading.Thread(target=run_install, args=(tier, skip), daemon=True)
|
t = threading.Thread(target=run_install, args=(tier, skip, license_data), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
self.send_json({"ok": True})
|
self.send_json({"ok": True})
|
||||||
else:
|
else:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user