feat: role-aware hub — service filtering, onboarding, upgrade path

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/af4088da-8845-4f7f-914f-259fd33884ed

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-05 03:55:20 +00:00
committed by GitHub
parent c28de5def9
commit 58966646c2
8 changed files with 283 additions and 19 deletions

View File

@@ -82,6 +82,62 @@
margin: 16px 0;
}
/* ── Sidebar: Upgrade button (Node role) ────────────────────────── */
.sidebar-upgrade-btn {
border-color: var(--accent-color);
background-color: rgba(137, 180, 250, 0.06);
margin-top: 8px;
}
.sidebar-upgrade-btn:hover {
background-color: rgba(137, 180, 250, 0.14);
border-color: var(--accent-color);
}
.sidebar-upgrade-btn .sidebar-support-hint {
color: var(--accent-color);
}
/* ── Upgrade modal ──────────────────────────────────────────────── */
.upgrade-dialog {
max-width: 480px;
}
.upgrade-info-box {
background-color: var(--card-color);
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 14px 18px;
margin-bottom: 14px;
}
.upgrade-info-title {
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.upgrade-info-list {
padding-left: 20px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.7;
margin: 0;
}
.upgrade-info-list a {
color: var(--accent-color);
}
.upgrade-rebuild-note {
font-style: italic;
color: var(--text-dim);
font-size: 0.82rem;
}
/* ── Tiles area ─────────────────────────────────────────────────── */
#tiles-area {

View File

@@ -33,6 +33,43 @@ if ($modal) $modal.addEventListener("click", function(e) { if (e.target === $mod
if ($credsModal) $credsModal.addEventListener("click", function(e) { if (e.target === $credsModal) closeCredsModal(); });
if ($supportModal) $supportModal.addEventListener("click", function(e) { if (e.target === $supportModal) closeSupportModal(); });
// Upgrade modal
if ($upgradeCloseBtn) $upgradeCloseBtn.addEventListener("click", closeUpgradeModal);
if ($upgradeCancelBtn) $upgradeCancelBtn.addEventListener("click", closeUpgradeModal);
if ($upgradeModal) $upgradeModal.addEventListener("click", function(e) { if (e.target === $upgradeModal) closeUpgradeModal(); });
// ── Upgrade modal functions ───────────────────────────────────────
function openUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.add("open");
}
function closeUpgradeModal() {
if ($upgradeModal) $upgradeModal.classList.remove("open");
}
async function doUpgradeToServer() {
var confirmBtn = $upgradeConfirmBtn;
if (confirmBtn) { confirmBtn.disabled = true; confirmBtn.textContent = "Upgrading…"; }
closeUpgradeModal();
// Reuse the rebuild modal to show progress
_rebuildFeatureName = "Server + Desktop";
_rebuildIsEnabling = true;
openRebuildModal();
try {
await apiFetch("/api/role/upgrade-to-server", { method: "POST" });
} catch (err) {
if ($rebuildStatus) $rebuildStatus.textContent = "✗ Upgrade failed: " + err.message;
if ($rebuildSpinner) $rebuildSpinner.classList.remove("spinning");
if ($rebuildClose) $rebuildClose.disabled = false;
if (confirmBtn) { confirmBtn.disabled = false; confirmBtn.textContent = "Yes, Upgrade"; }
}
}
if ($upgradeConfirmBtn) $upgradeConfirmBtn.addEventListener("click", doUpgradeToServer);
// ── Init ──────────────────────────────────────────────────────────
async function init() {
@@ -49,6 +86,7 @@ async function init() {
try {
var cfg = await apiFetch("/api/config");
_currentRole = cfg.role || "server_plus_desktop";
if (cfg.category_order) {
for (var i = 0; i < cfg.category_order.length; i++) {
_categoryLabels[cfg.category_order[i][0]] = cfg.category_order[i][1];

View File

@@ -15,6 +15,9 @@ let _supportStatus = null; // last fetched /api/support/status payload
let _walletUnlockTimerInt = null;
let _cachedExternalIp = null;
// Current role (set during init from /api/config)
let _currentRole = "server_plus_desktop";
// Feature Manager state
let _featuresData = null;
let _rebuildLog = "";
@@ -89,5 +92,11 @@ const $portReqModal = document.getElementById("port-requirements-modal");
const $portReqBody = document.getElementById("port-req-body");
const $portReqClose = document.getElementById("port-req-close-btn");
// Upgrade modal (Node → Server+Desktop)
const $upgradeModal = document.getElementById("upgrade-modal");
const $upgradeConfirmBtn = document.getElementById("upgrade-confirm-btn");
const $upgradeCancelBtn = document.getElementById("upgrade-cancel-btn");
const $upgradeCloseBtn = document.getElementById("upgrade-close-btn");
// System status banner
// (removed — health is now shown per-tile via the composite health field)

View File

@@ -71,6 +71,20 @@ function renderSidebarSupport(supportServices) {
backupBtn.addEventListener("click", function() { openBackupModal(); });
$sidebarSupport.appendChild(backupBtn);
// ── Upgrade button (Node role only)
if (_currentRole === "node") {
var upgradeBtn = document.createElement("button");
upgradeBtn.className = "sidebar-support-btn sidebar-upgrade-btn";
upgradeBtn.innerHTML =
'<span class="sidebar-support-icon">🚀</span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">Upgrade to Full Server</span>' +
'<span class="sidebar-support-hint">Unlock all services</span>' +
'</span>';
upgradeBtn.addEventListener("click", function() { openUpgradeModal(); });
$sidebarSupport.appendChild(upgradeBtn);
}
var hr = document.createElement("hr");
hr.className = "sidebar-divider";
$sidebarSupport.appendChild(hr);

View File

@@ -6,6 +6,16 @@
const TOTAL_STEPS = 4;
// Steps to skip per role (steps 2 and 3 involve domain/port setup)
const ROLE_SKIP_STEPS = {
"desktop": [2, 3],
"node": [2, 3],
};
// ── Role state (loaded at init) ───────────────────────────────────
var _onboardingRole = "server_plus_desktop";
// Domains that may need configuration, with service unit mapping for enabled check
const DOMAIN_DEFS = [
{ name: "matrix", label: "Matrix (Synapse)", unit: "matrix-synapse.service", needsDdns: true },
@@ -83,6 +93,22 @@ function showStep(step) {
if (step === 3) loadStep3();
}
// Return the next step number, skipping over role-excluded steps
function nextStep(current) {
var skip = ROLE_SKIP_STEPS[_onboardingRole] || [];
var next = current + 1;
while (next < TOTAL_STEPS && skip.indexOf(next) !== -1) next++;
return next;
}
// Return the previous step number, skipping over role-excluded steps
function prevStep(current) {
var skip = ROLE_SKIP_STEPS[_onboardingRole] || [];
var prev = current - 1;
while (prev > 1 && skip.indexOf(prev) !== -1) prev--;
return prev;
}
// ── Step 1: Welcome ───────────────────────────────────────────────
async function loadStep1() {
@@ -319,9 +345,9 @@ async function completeOnboarding() {
// ── Event wiring ──────────────────────────────────────────────────
function wireNavButtons() {
// Step 1 → 2
// Step 1 → next (may skip 2+3 for desktop/node)
var s1next = document.getElementById("step-1-next");
if (s1next) s1next.addEventListener("click", function() { showStep(2); });
if (s1next) s1next.addEventListener("click", function() { showStep(nextStep(1)); });
// Step 2 → 3 (save first)
var s2next = document.getElementById("step-2-next");
@@ -331,12 +357,12 @@ function wireNavButtons() {
await saveStep2();
s2next.disabled = false;
s2next.textContent = "Save & Continue →";
showStep(3);
showStep(nextStep(2));
});
// Step 3 → 4 (Complete)
var s3next = document.getElementById("step-3-next");
if (s3next) s3next.addEventListener("click", function() { showStep(4); });
if (s3next) s3next.addEventListener("click", function() { showStep(nextStep(3)); });
// Step 4: finish
var s4finish = document.getElementById("step-4-finish");
@@ -345,7 +371,7 @@ function wireNavButtons() {
// Back buttons
document.querySelectorAll(".onboarding-btn-back").forEach(function(btn) {
var prev = parseInt(btn.dataset.prev, 10);
btn.addEventListener("click", function() { showStep(prev); });
btn.addEventListener("click", function() { showStep(prevStep(prev + 1)); });
});
}
@@ -361,6 +387,12 @@ document.addEventListener("DOMContentLoaded", async function() {
}
} catch (_) {}
// Load role so step-skipping is applied before wiring nav buttons
try {
var cfg = await apiFetch("/api/config");
if (cfg.role) _onboardingRole = cfg.role;
} catch (_) {}
wireNavButtons();
updateProgress(1);
loadStep1();