Merge pull request #124 from naturallaw777/copilot/add-password-creation-step-onboarding
Add password creation step to first-boot onboarding wizard
This commit is contained in:
@@ -2953,6 +2953,17 @@ async def api_security_status():
|
|||||||
return {"status": status, "warning": warning}
|
return {"status": status, "warning": warning}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/security/password-is-default")
|
||||||
|
async def api_password_is_default():
|
||||||
|
"""Check if the free account password is still the factory default."""
|
||||||
|
try:
|
||||||
|
with open("/var/lib/secrets/free-password", "r") as f:
|
||||||
|
current = f.read().strip()
|
||||||
|
return {"is_default": current == "free"}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"is_default": True}
|
||||||
|
|
||||||
|
|
||||||
# ── System password change ────────────────────────────────────────
|
# ── System password change ────────────────────────────────────────
|
||||||
|
|
||||||
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
|
FREE_PASSWORD_FILE = "/var/lib/secrets/free-password"
|
||||||
|
|||||||
@@ -575,6 +575,110 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Password step (Step 2) ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.onboarding-password-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-btn);
|
||||||
|
background-color: var(--card-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-family: 'Cantarell', 'Inter', 'Segoe UI', sans-serif;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-toggle {
|
||||||
|
padding: 6px 10px;
|
||||||
|
background-color: var(--card-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-btn);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
transition: background-color 0.15s, border-color 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-toggle:hover {
|
||||||
|
background-color: rgba(137, 180, 250, 0.12);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-hint {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-warning {
|
||||||
|
padding: 10px 14px;
|
||||||
|
background-color: rgba(229, 165, 10, 0.1);
|
||||||
|
border: 1px solid rgba(229, 165, 10, 0.35);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--yellow);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-success {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: rgba(166, 227, 161, 0.1);
|
||||||
|
border: 1px solid rgba(166, 227, 161, 0.35);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
color: var(--green);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-optional {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-optional > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-color);
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-optional > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-optional > summary::before {
|
||||||
|
content: '▶ ';
|
||||||
|
font-size: 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-password-optional[open] > summary::before {
|
||||||
|
content: '▼ ';
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Reboot overlay ─────────────────────────────────────────────── */
|
/* ── Reboot overlay ─────────────────────────────────────────────── */
|
||||||
|
|
||||||
.reboot-overlay {
|
.reboot-overlay {
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
|
/* Sovran_SystemsOS Hub — First-Boot Onboarding Wizard
|
||||||
Drives the 4-step post-install setup flow. */
|
Drives the 5-step post-install setup flow. */
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// ── Constants ─────────────────────────────────────────────────────
|
// ── Constants ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const TOTAL_STEPS = 4;
|
const TOTAL_STEPS = 5;
|
||||||
|
|
||||||
// Steps to skip per role (steps 2 and 3 involve domain/port setup)
|
// Steps to skip per role (steps 3 and 4 involve domain/port setup)
|
||||||
|
// Step 2 (password) is NEVER skipped — all roles need it.
|
||||||
const ROLE_SKIP_STEPS = {
|
const ROLE_SKIP_STEPS = {
|
||||||
"desktop": [2, 3],
|
"desktop": [3, 4],
|
||||||
"node": [2, 3],
|
"node": [3, 4],
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Role state (loaded at init) ───────────────────────────────────
|
// ── Role state (loaded at init) ───────────────────────────────────
|
||||||
|
|
||||||
var _onboardingRole = "server_plus_desktop";
|
var _onboardingRole = "server_plus_desktop";
|
||||||
|
|
||||||
|
// Password default state (loaded at step 2)
|
||||||
|
var _passwordIsDefault = true;
|
||||||
|
|
||||||
// Domains that may need configuration, with service unit mapping for enabled check
|
// Domains that may need configuration, with service unit mapping for enabled check
|
||||||
const DOMAIN_DEFS = [
|
const DOMAIN_DEFS = [
|
||||||
{ name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true },
|
{ name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true },
|
||||||
@@ -91,6 +95,8 @@ function showStep(step) {
|
|||||||
// Lazy-load step content
|
// Lazy-load step content
|
||||||
if (step === 2) loadStep2();
|
if (step === 2) loadStep2();
|
||||||
if (step === 3) loadStep3();
|
if (step === 3) loadStep3();
|
||||||
|
if (step === 4) loadStep4();
|
||||||
|
// Step 5 (Complete) is static — no lazy-load needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the next step number, skipping over role-excluded steps
|
// Return the next step number, skipping over role-excluded steps
|
||||||
@@ -119,12 +125,135 @@ async function loadStep1() {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 2: Domain Configuration ─────────────────────────────────
|
// ── Step 2: Create Your Password ─────────────────────────────────
|
||||||
|
|
||||||
async function loadStep2() {
|
async function loadStep2() {
|
||||||
var body = document.getElementById("step-2-body");
|
var body = document.getElementById("step-2-body");
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
|
|
||||||
|
var nextBtn = document.getElementById("step-2-next");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var result = await apiFetch("/api/security/password-is-default");
|
||||||
|
_passwordIsDefault = result.is_default !== false;
|
||||||
|
} catch (_) {
|
||||||
|
_passwordIsDefault = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_passwordIsDefault) {
|
||||||
|
// Factory-sealed scenario: password must be set before continuing
|
||||||
|
if (nextBtn) nextBtn.textContent = "Set Password & Continue \u2192";
|
||||||
|
|
||||||
|
body.innerHTML =
|
||||||
|
'<div class="onboarding-password-group">' +
|
||||||
|
'<label class="onboarding-domain-label" for="pw-new">New Password</label>' +
|
||||||
|
'<div class="onboarding-password-input-wrap">' +
|
||||||
|
'<input class="onboarding-password-input" type="password" id="pw-new" autocomplete="new-password" placeholder="At least 8 characters" />' +
|
||||||
|
'<button type="button" class="onboarding-password-toggle" data-target="pw-new" aria-label="Show password">👁</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p class="onboarding-password-hint">Minimum 8 characters</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="onboarding-password-group">' +
|
||||||
|
'<label class="onboarding-domain-label" for="pw-confirm">Confirm Password</label>' +
|
||||||
|
'<div class="onboarding-password-input-wrap">' +
|
||||||
|
'<input class="onboarding-password-input" type="password" id="pw-confirm" autocomplete="new-password" placeholder="Re-enter your password" />' +
|
||||||
|
'<button type="button" class="onboarding-password-toggle" data-target="pw-confirm" aria-label="Show password">👁</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="onboarding-password-warning">⚠️ Write this password down — it cannot be recovered.</div>';
|
||||||
|
|
||||||
|
// Wire show/hide toggles
|
||||||
|
body.querySelectorAll(".onboarding-password-toggle").forEach(function(btn) {
|
||||||
|
btn.addEventListener("click", function() {
|
||||||
|
var inp = document.getElementById(btn.dataset.target);
|
||||||
|
if (inp) inp.type = (inp.type === "password") ? "text" : "password";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// DIY install scenario: password already set by installer
|
||||||
|
if (nextBtn) nextBtn.textContent = "Continue \u2192";
|
||||||
|
|
||||||
|
body.innerHTML =
|
||||||
|
'<div class="onboarding-password-success">✅ Your password was already set during installation.</div>' +
|
||||||
|
'<details class="onboarding-password-optional">' +
|
||||||
|
'<summary>Change it anyway</summary>' +
|
||||||
|
'<div style="margin-top:14px;">' +
|
||||||
|
'<div class="onboarding-password-group">' +
|
||||||
|
'<label class="onboarding-domain-label" for="pw-new">New Password</label>' +
|
||||||
|
'<div class="onboarding-password-input-wrap">' +
|
||||||
|
'<input class="onboarding-password-input" type="password" id="pw-new" autocomplete="new-password" placeholder="At least 8 characters" />' +
|
||||||
|
'<button type="button" class="onboarding-password-toggle" data-target="pw-new" aria-label="Show password">👁</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p class="onboarding-password-hint">Minimum 8 characters</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="onboarding-password-group">' +
|
||||||
|
'<label class="onboarding-domain-label" for="pw-confirm">Confirm Password</label>' +
|
||||||
|
'<div class="onboarding-password-input-wrap">' +
|
||||||
|
'<input class="onboarding-password-input" type="password" id="pw-confirm" autocomplete="new-password" placeholder="Re-enter your password" />' +
|
||||||
|
'<button type="button" class="onboarding-password-toggle" data-target="pw-confirm" aria-label="Show password">👁</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="onboarding-password-warning">⚠️ Write this password down — it cannot be recovered.</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</details>';
|
||||||
|
|
||||||
|
// Wire show/hide toggles
|
||||||
|
body.querySelectorAll(".onboarding-password-toggle").forEach(function(btn) {
|
||||||
|
btn.addEventListener("click", function() {
|
||||||
|
var inp = document.getElementById(btn.dataset.target);
|
||||||
|
if (inp) inp.type = (inp.type === "password") ? "text" : "password";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStep2() {
|
||||||
|
var newPw = document.getElementById("pw-new");
|
||||||
|
var confirmPw = document.getElementById("pw-confirm");
|
||||||
|
|
||||||
|
// If no fields visible or both empty and password already set → skip
|
||||||
|
if (!newPw || !newPw.value.trim()) {
|
||||||
|
if (!_passwordIsDefault) return true; // already set, no change requested
|
||||||
|
setStatus("step-2-status", "⚠ Please enter a password.", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pw = newPw.value;
|
||||||
|
var cpw = confirmPw ? confirmPw.value : "";
|
||||||
|
|
||||||
|
if (pw.length < 8) {
|
||||||
|
setStatus("step-2-status", "⚠ Password must be at least 8 characters.", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pw !== cpw) {
|
||||||
|
setStatus("step-2-status", "⚠ Passwords do not match.", "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("step-2-status", "Saving password…", "info");
|
||||||
|
try {
|
||||||
|
await apiFetch("/api/change-password", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ new_password: pw, confirm_password: cpw }),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setStatus("step-2-status", "⚠ " + err.message, "error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus("step-2-status", "✓ Password saved", "ok");
|
||||||
|
_passwordIsDefault = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Step 3: Domain Configuration ─────────────────────────────────
|
||||||
|
|
||||||
|
async function loadStep3() {
|
||||||
|
var body = document.getElementById("step-3-body");
|
||||||
|
if (!body) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch services, domains, and network info in parallel
|
// Fetch services, domains, and network info in parallel
|
||||||
var results = await Promise.all([
|
var results = await Promise.all([
|
||||||
@@ -194,8 +323,8 @@ async function loadStep2() {
|
|||||||
body.innerHTML = html;
|
body.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveStep2() {
|
async function saveStep3() {
|
||||||
setStatus("step-2-status", "Saving domains…", "info");
|
setStatus("step-3-status", "Saving domains…", "info");
|
||||||
var errors = [];
|
var errors = [];
|
||||||
|
|
||||||
// Save each domain input
|
// Save each domain input
|
||||||
@@ -235,18 +364,18 @@ async function saveStep2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
setStatus("step-2-status", "⚠ Some errors: " + errors.join("; "), "error");
|
setStatus("step-3-status", "⚠ Some errors: " + errors.join("; "), "error");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus("step-2-status", "✓ Saved", "ok");
|
setStatus("step-3-status", "✓ Saved", "ok");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 3: Port Forwarding ───────────────────────────────────────
|
// ── Step 4: Port Forwarding ───────────────────────────────────────
|
||||||
|
|
||||||
async function loadStep3() {
|
async function loadStep4() {
|
||||||
var body = document.getElementById("step-3-body");
|
var body = document.getElementById("step-4-body");
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>';
|
body.innerHTML = '<p class="onboarding-loading">Checking ports…</p>';
|
||||||
|
|
||||||
@@ -327,10 +456,10 @@ async function loadStep3() {
|
|||||||
body.innerHTML = html;
|
body.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 4: Complete ──────────────────────────────────────────────
|
// ── Step 5: Complete ──────────────────────────────────────────────
|
||||||
|
|
||||||
async function completeOnboarding() {
|
async function completeOnboarding() {
|
||||||
var btn = document.getElementById("step-4-finish");
|
var btn = document.getElementById("step-5-finish");
|
||||||
if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; }
|
if (btn) { btn.disabled = true; btn.textContent = "Finishing…"; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -345,28 +474,40 @@ async function completeOnboarding() {
|
|||||||
// ── Event wiring ──────────────────────────────────────────────────
|
// ── Event wiring ──────────────────────────────────────────────────
|
||||||
|
|
||||||
function wireNavButtons() {
|
function wireNavButtons() {
|
||||||
// Step 1 → next (may skip 2+3 for desktop/node)
|
// Step 1 → next
|
||||||
var s1next = document.getElementById("step-1-next");
|
var s1next = document.getElementById("step-1-next");
|
||||||
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
|
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
|
||||||
|
|
||||||
// Step 2 → 3 (save first)
|
// Step 2 → 3 (save password first)
|
||||||
var s2next = document.getElementById("step-2-next");
|
var s2next = document.getElementById("step-2-next");
|
||||||
if (s2next) s2next.addEventListener("click", async function() {
|
if (s2next) s2next.addEventListener("click", async function() {
|
||||||
s2next.disabled = true;
|
s2next.disabled = true;
|
||||||
|
var origText = s2next.textContent;
|
||||||
s2next.textContent = "Saving…";
|
s2next.textContent = "Saving…";
|
||||||
await saveStep2();
|
var ok = await saveStep2();
|
||||||
s2next.disabled = false;
|
s2next.disabled = false;
|
||||||
s2next.textContent = "Save & Continue →";
|
s2next.textContent = origText;
|
||||||
showStep(nextStep(2));
|
if (ok) showStep(nextStep(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 3 → 4 (Complete)
|
// Step 3 → 4 (save domains first)
|
||||||
var s3next = document.getElementById("step-3-next");
|
var s3next = document.getElementById("step-3-next");
|
||||||
if (s3next) s3next.addEventListener("click", function() { showStep(nextStep(3)); });
|
if (s3next) s3next.addEventListener("click", async function() {
|
||||||
|
s3next.disabled = true;
|
||||||
|
s3next.textContent = "Saving…";
|
||||||
|
await saveStep3();
|
||||||
|
s3next.disabled = false;
|
||||||
|
s3next.textContent = "Save & Continue →";
|
||||||
|
showStep(nextStep(3));
|
||||||
|
});
|
||||||
|
|
||||||
// Step 4: finish
|
// Step 4 → 5 (Complete)
|
||||||
var s4finish = document.getElementById("step-4-finish");
|
var s4next = document.getElementById("step-4-next");
|
||||||
if (s4finish) s4finish.addEventListener("click", completeOnboarding);
|
if (s4next) s4next.addEventListener("click", function() { showStep(nextStep(4)); });
|
||||||
|
|
||||||
|
// Step 5: finish
|
||||||
|
var s5finish = document.getElementById("step-5-finish");
|
||||||
|
if (s5finish) s5finish.addEventListener("click", completeOnboarding);
|
||||||
|
|
||||||
// Back buttons
|
// Back buttons
|
||||||
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
|
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
<span class="onboarding-step-dot" data-step="3">3</span>
|
<span class="onboarding-step-dot" data-step="3">3</span>
|
||||||
<span class="onboarding-step-connector"></span>
|
<span class="onboarding-step-connector"></span>
|
||||||
<span class="onboarding-step-dot" data-step="4">4</span>
|
<span class="onboarding-step-dot" data-step="4">4</span>
|
||||||
|
<span class="onboarding-step-connector"></span>
|
||||||
|
<span class="onboarding-step-dot" data-step="5">5</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step panels -->
|
<!-- Step panels -->
|
||||||
@@ -70,8 +72,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Step 2: Domain Configuration ── -->
|
<!-- ── Step 2: Create Your Password ── -->
|
||||||
<div class="onboarding-panel" id="step-2" style="display:none">
|
<div class="onboarding-panel" id="step-2" style="display:none">
|
||||||
|
<div class="onboarding-step-header">
|
||||||
|
<span class="onboarding-step-icon">🔒</span>
|
||||||
|
<h2 class="onboarding-step-title">Create Your Password</h2>
|
||||||
|
<p class="onboarding-step-desc">
|
||||||
|
Choose a strong password for your <strong>'free'</strong> user account. This will be your login password for the desktop, SSH, and the Hub.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="onboarding-card" id="step-2-body">
|
||||||
|
<p class="onboarding-loading">Checking password status…</p>
|
||||||
|
</div>
|
||||||
|
<div id="step-2-status" class="onboarding-save-status"></div>
|
||||||
|
<div class="onboarding-footer">
|
||||||
|
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
|
||||||
|
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
|
||||||
|
Set Password & Continue →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Step 3: Domain Configuration ── -->
|
||||||
|
<div class="onboarding-panel" id="step-3" style="display:none">
|
||||||
<div class="onboarding-step-header">
|
<div class="onboarding-step-header">
|
||||||
<span class="onboarding-step-icon">🌐</span>
|
<span class="onboarding-step-icon">🌐</span>
|
||||||
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
<h2 class="onboarding-step-title">Domain Configuration</h2>
|
||||||
@@ -82,20 +105,20 @@
|
|||||||
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
|
Finally, paste the DDNS curl command from your Njal.la dashboard for each service below.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding-card onboarding-card--scroll" id="step-2-body">
|
<div class="onboarding-card onboarding-card--scroll" id="step-3-body">
|
||||||
<p class="onboarding-loading">Loading service information…</p>
|
<p class="onboarding-loading">Loading service information…</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="step-2-status" class="onboarding-save-status"></div>
|
<div id="step-3-status" class="onboarding-save-status"></div>
|
||||||
<div class="onboarding-footer">
|
<div class="onboarding-footer">
|
||||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="1">← Back</button>
|
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
||||||
<button class="btn btn-primary onboarding-btn-next" id="step-2-next">
|
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
||||||
Save & Continue →
|
Save & Continue →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Step 3: Port Forwarding ── -->
|
<!-- ── Step 4: Port Forwarding ── -->
|
||||||
<div class="onboarding-panel" id="step-3" style="display:none">
|
<div class="onboarding-panel" id="step-4" style="display:none">
|
||||||
<div class="onboarding-step-header">
|
<div class="onboarding-step-header">
|
||||||
<span class="onboarding-step-icon">🔌</span>
|
<span class="onboarding-step-icon">🔌</span>
|
||||||
<h2 class="onboarding-step-title">Port Forwarding Check</h2>
|
<h2 class="onboarding-step-title">Port Forwarding Check</h2>
|
||||||
@@ -104,19 +127,19 @@
|
|||||||
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
|
<strong>Ports 80 and 443 must be open for SSL certificates to work.</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding-card onboarding-card--ports" id="step-3-body">
|
<div class="onboarding-card onboarding-card--ports" id="step-4-body">
|
||||||
<p class="onboarding-loading">Checking ports…</p>
|
<p class="onboarding-loading">Checking ports…</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding-footer">
|
<div class="onboarding-footer">
|
||||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="2">← Back</button>
|
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
||||||
<button class="btn btn-primary onboarding-btn-next" id="step-3-next">
|
<button class="btn btn-primary onboarding-btn-next" id="step-4-next">
|
||||||
Continue →
|
Continue →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Step 4: Complete ── -->
|
<!-- ── Step 5: Complete ── -->
|
||||||
<div class="onboarding-panel" id="step-4" style="display:none">
|
<div class="onboarding-panel" id="step-5" style="display:none">
|
||||||
<div class="onboarding-hero">
|
<div class="onboarding-hero">
|
||||||
<div class="onboarding-logo">✅</div>
|
<div class="onboarding-logo">✅</div>
|
||||||
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
|
<h1 class="onboarding-title">Your Sovran_SystemsOS is Ready!</h1>
|
||||||
@@ -128,13 +151,14 @@
|
|||||||
monitor your services, manage credentials, and make changes at any time.
|
monitor your services, manage credentials, and make changes at any time.
|
||||||
</p>
|
</p>
|
||||||
<ul class="onboarding-checklist" id="onboarding-checklist">
|
<ul class="onboarding-checklist" id="onboarding-checklist">
|
||||||
|
<li>✅ Password configured</li>
|
||||||
<li>✅ Domain configuration saved</li>
|
<li>✅ Domain configuration saved</li>
|
||||||
<li>✅ Port forwarding reviewed</li>
|
<li>✅ Port forwarding reviewed</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="onboarding-footer">
|
<div class="onboarding-footer">
|
||||||
<button class="btn btn-close-modal onboarding-btn-back" data-prev="3">← Back</button>
|
<button class="btn btn-close-modal onboarding-btn-back" data-prev="4">← Back</button>
|
||||||
<button class="btn btn-primary" id="step-4-finish">
|
<button class="btn btn-primary" id="step-5-finish">
|
||||||
Go to Dashboard →
|
Go to Dashboard →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user