diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..7c953211 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,14 @@ +# Gitleaks configuration for BetterDesk +# Suppresses false positives from auto-generated lock files + +title = "BetterDesk Gitleaks Config" + +[allowlist] + description = "Allowlisted files and patterns" + + # Cargo.lock files contain crate checksums and dependency names like + # "crypto_secretbox" that trigger generic-api-key false positives. + paths = [ + '''.*Cargo\.lock$''', + '''.*package-lock\.json$''', + ] diff --git a/web-nodejs/data/auth.db-shm b/web-nodejs/data/auth.db-shm new file mode 100644 index 00000000..a89befe0 Binary files /dev/null and b/web-nodejs/data/auth.db-shm differ diff --git a/web-nodejs/data/auth.db-wal b/web-nodejs/data/auth.db-wal new file mode 100644 index 00000000..15c419c9 Binary files /dev/null and b/web-nodejs/data/auth.db-wal differ diff --git a/web-nodejs/data/betterdesk.db-shm b/web-nodejs/data/betterdesk.db-shm new file mode 100644 index 00000000..600e9cbb Binary files /dev/null and b/web-nodejs/data/betterdesk.db-shm differ diff --git a/web-nodejs/data/betterdesk.db-wal b/web-nodejs/data/betterdesk.db-wal new file mode 100644 index 00000000..2bab5816 Binary files /dev/null and b/web-nodejs/data/betterdesk.db-wal differ diff --git a/web-nodejs/public/css/beta31.css b/web-nodejs/public/css/beta31.css new file mode 100644 index 00000000..210432b2 --- /dev/null +++ b/web-nodejs/public/css/beta31.css @@ -0,0 +1,510 @@ +/** + * BetterDesk Console — Beta 3.1 Panel + * ISPmanager-inspired layout with persistent sidebar + content area + */ + +/* ═══════════════════════════════════════════════════════════════════ + NAVBAR TOGGLE BUTTON + ═══════════════════════════════════════════════════════════════════ */ + +.beta31-toggle-btn { + position: relative; + display: flex !important; +} + +/* ═══════════════════════════════════════════════════════════════════ + BETA 3.1 SHELL — full-screen overlay + ═══════════════════════════════════════════════════════════════════ */ + +.beta31-shell { + position: fixed; + inset: 0; + z-index: 250; + display: none; + background: var(--b31-bg, #f0f2f5); + font-family: var(--font-family); +} + +.beta31-shell.active { + display: flex; + flex-direction: column; +} + +/* ── Top bar ── */ +.b31-topbar { + height: 48px; + min-height: 48px; + background: var(--b31-topbar-bg, #2b579a); + color: #fff; + display: flex; + align-items: center; + padding: 0 16px; + gap: 12px; + z-index: 2; + box-shadow: 0 1px 3px rgba(0,0,0,.18); +} + +.b31-topbar-logo { + display: flex; + align-items: center; + gap: 8px; + font-weight: 700; + font-size: 15px; + letter-spacing: .02em; + white-space: nowrap; +} + +.b31-topbar-logo .material-icons { font-size: 22px; } + +.b31-topbar-badge { + background: rgba(255,255,255,.2); + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + font-weight: 600; + letter-spacing: .05em; +} + +.b31-topbar-tabs { + display: flex; + align-items: center; + gap: 2px; + margin-left: 24px; + height: 100%; +} + +.b31-topbar-tab { + background: none; + border: none; + color: rgba(255,255,255,.7); + font-size: 13px; + padding: 0 14px; + height: 100%; + cursor: pointer; + transition: all .15s; + border-bottom: 2px solid transparent; + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; +} + +.b31-topbar-tab:hover { color: #fff; background: rgba(255,255,255,.08); } +.b31-topbar-tab.active { + color: #fff; + border-bottom-color: #fff; + background: rgba(255,255,255,.1); +} + +.b31-topbar-tab .material-icons { font-size: 18px; } + +.b31-topbar-spacer { flex: 1; } + +.b31-topbar-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.b31-topbar-btn { + background: none; + border: none; + color: rgba(255,255,255,.75); + padding: 6px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + transition: all .15s; +} +.b31-topbar-btn:hover { background: rgba(255,255,255,.12); color: #fff; } +.b31-topbar-btn .material-icons { font-size: 20px; } + +/* ── Body: sidebar + content ── */ +.b31-body { + flex: 1; + display: flex; + overflow: hidden; +} + +/* ── Sidebar ── */ +.b31-sidebar { + width: 220px; + min-width: 220px; + background: var(--b31-sidebar-bg, #fff); + border-right: 1px solid var(--b31-border, #dde1e6); + display: flex; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; +} + +.b31-sidebar-section { + padding: 6px 0; + border-bottom: 1px solid var(--b31-border, #dde1e6); +} + +.b31-sidebar-section:last-child { border-bottom: none; } + +.b31-sidebar-heading { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .06em; + color: var(--b31-muted, #8a8f9a); + padding: 10px 16px 4px; +} + +.b31-sidebar-item { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 16px; + font-size: 13px; + color: var(--b31-text, #333); + cursor: pointer; + transition: all .12s; + border: none; + background: none; + width: 100%; + text-align: left; + border-left: 3px solid transparent; +} + +.b31-sidebar-item:hover { + background: var(--b31-hover, #f5f7fa); + color: var(--b31-primary, #2b579a); +} + +.b31-sidebar-item.active { + background: var(--b31-active-bg, #e8eef7); + color: var(--b31-primary, #2b579a); + font-weight: 600; + border-left-color: var(--b31-primary, #2b579a); +} + +.b31-sidebar-item .material-icons { + font-size: 20px; + opacity: .7; + flex-shrink: 0; +} + +.b31-sidebar-item.active .material-icons { opacity: 1; } + +.b31-sidebar-item-badge { + margin-left: auto; + background: var(--b31-primary, #2b579a); + color: #fff; + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + font-weight: 600; +} + +/* ── Content area ── */ +.b31-content { + flex: 1; + overflow-y: auto; + padding: 24px; + background: var(--b31-bg, #f0f2f5); +} + +/* ── Content cards (ISPmanager-style collapsible sections) ── */ +.b31-card { + background: var(--b31-card-bg, #fff); + border: 1px solid var(--b31-border, #dde1e6); + border-radius: 6px; + margin-bottom: 16px; + box-shadow: 0 1px 2px rgba(0,0,0,.04); +} + +.b31-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + cursor: pointer; + user-select: none; + border-bottom: 1px solid var(--b31-border, #dde1e6); + transition: background .12s; +} + +.b31-card-header:hover { background: var(--b31-hover, #f5f7fa); } + +.b31-card-header-left { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + color: var(--b31-text, #333); +} + +.b31-card-header-left .material-icons { + font-size: 18px; + color: var(--b31-primary, #2b579a); +} + +.b31-card-header-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.b31-card-action-btn { + background: none; + border: none; + color: var(--b31-muted, #8a8f9a); + padding: 4px; + border-radius: 4px; + cursor: pointer; + display: flex; + transition: all .12s; +} +.b31-card-action-btn:hover { + background: var(--b31-hover, #f5f7fa); + color: var(--b31-primary, #2b579a); +} +.b31-card-action-btn .material-icons { font-size: 18px; } + +.b31-card-body { padding: 16px; } + +.b31-card.collapsed .b31-card-body { display: none; } +.b31-card.collapsed .b31-card-header { border-bottom: none; } + +/* ── Info grid (label-value pairs like ISPmanager system info) ── */ +.b31-info-grid { + display: grid; + grid-template-columns: 180px 1fr; + gap: 0; +} + +.b31-info-label, +.b31-info-value { + padding: 8px 12px; + font-size: 13px; + border-bottom: 1px solid var(--b31-border-light, #eef0f3); +} + +.b31-info-label { + color: var(--b31-muted, #8a8f9a); + font-weight: 500; +} + +.b31-info-value { + color: var(--b31-text, #333); +} + +.b31-info-value a { + color: var(--b31-primary, #2b579a); + text-decoration: none; +} +.b31-info-value a:hover { text-decoration: underline; } + +/* ── Table styles ── */ +.b31-table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.b31-table th { + text-align: left; + padding: 8px 12px; + font-weight: 600; + color: var(--b31-muted, #8a8f9a); + font-size: 12px; + text-transform: uppercase; + letter-spacing: .04em; + border-bottom: 2px solid var(--b31-border, #dde1e6); + white-space: nowrap; +} + +.b31-table td { + padding: 8px 12px; + border-bottom: 1px solid var(--b31-border-light, #eef0f3); + color: var(--b31-text, #333); +} + +.b31-table tbody tr:hover { + background: var(--b31-hover, #f5f7fa); +} + +/* ── Quick action bar (like ISPmanager taskbar) ── */ +.b31-taskbar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: var(--b31-card-bg, #fff); + border: 1px solid var(--b31-border, #dde1e6); + border-radius: 6px; + margin-bottom: 16px; +} + +.b31-taskbar-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 8px 16px; + border-radius: 6px; + cursor: pointer; + transition: all .12s; + text-decoration: none; + color: var(--b31-text, #333); + font-size: 12px; + background: none; + border: none; +} +.b31-taskbar-item:hover { background: var(--b31-hover, #f5f7fa); color: var(--b31-primary, #2b579a); } +.b31-taskbar-item .material-icons { font-size: 28px; color: var(--b31-primary, #2b579a); } + +/* ── Status indicators ── */ +.b31-status { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 500; +} + +.b31-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} +.b31-status.online .b31-status-dot { background: #2ea44f; } +.b31-status.offline .b31-status-dot { background: #8b949e; } +.b31-status.error .b31-status-dot { background: #f85149; } +.b31-status.warning .b31-status-dot { background: #d29922; } + +/* ── Buttons ── */ +.b31-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + font-size: 13px; + border-radius: 4px; + border: 1px solid var(--b31-border, #dde1e6); + background: var(--b31-card-bg, #fff); + color: var(--b31-text, #333); + cursor: pointer; + transition: all .12s; + font-weight: 500; +} +.b31-btn:hover { background: var(--b31-hover, #f5f7fa); border-color: var(--b31-primary, #2b579a); } +.b31-btn .material-icons { font-size: 16px; } + +.b31-btn-primary { + background: var(--b31-primary, #2b579a); + color: #fff; + border-color: var(--b31-primary, #2b579a); +} +.b31-btn-primary:hover { background: #1e4280; } + +/* ── Loading state ── */ +.b31-loading { + display: flex; + align-items: center; + justify-content: center; + padding: 60px; + color: var(--b31-muted, #8a8f9a); + font-size: 14px; + gap: 10px; +} + +.b31-loading .material-icons { + animation: b31spin 1s linear infinite; +} + +@keyframes b31spin { + to { transform: rotate(360deg); } +} + +/* ── Page header inside content area ── */ +.b31-page-header { + margin-bottom: 20px; +} + +.b31-page-title { + font-size: 20px; + font-weight: 700; + color: var(--b31-text, #333); + margin-bottom: 4px; +} + +.b31-page-subtitle { + font-size: 13px; + color: var(--b31-muted, #8a8f9a); +} + +/* ── Stats row ── */ +.b31-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + margin-bottom: 16px; +} + +.b31-stat-card { + background: var(--b31-card-bg, #fff); + border: 1px solid var(--b31-border, #dde1e6); + border-radius: 6px; + padding: 16px; + display: flex; + align-items: center; + gap: 12px; +} + +.b31-stat-icon { + width: 40px; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} +.b31-stat-icon.blue { background: rgba(43,87,154,.1); color: #2b579a; } +.b31-stat-icon.green { background: rgba(46,164,79,.1); color: #2ea44f; } +.b31-stat-icon.red { background: rgba(248,81,73,.1); color: #f85149; } +.b31-stat-icon.yellow { background: rgba(210,153,34,.1); color: #d29922; } +.b31-stat-icon .material-icons { font-size: 22px; } + +.b31-stat-value { font-size: 22px; font-weight: 700; color: var(--b31-text, #333); } +.b31-stat-label { font-size: 12px; color: var(--b31-muted, #8a8f9a); } + +/* ═══════════════════════════════════════════════════════════════════ + DARK MODE overrides + ═══════════════════════════════════════════════════════════════════ */ + +.beta31-shell.b31-dark { + --b31-bg: #0d1117; + --b31-topbar-bg: #161b22; + --b31-sidebar-bg: #161b22; + --b31-card-bg: #1c2128; + --b31-border: #30363d; + --b31-border-light: #262c36; + --b31-text: #e6edf3; + --b31-muted: #8b949e; + --b31-hover: rgba(255,255,255,.04); + --b31-active-bg: rgba(88,166,255,.12); + --b31-primary: #58a6ff; +} + +/* ═══════════════════════════════════════════════════════════════════ + RESPONSIVE + ═══════════════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + .b31-sidebar { width: 56px; min-width: 56px; } + .b31-sidebar-heading, + .b31-sidebar-item span:not(.material-icons), + .b31-sidebar-item-badge { display: none; } + .b31-sidebar-item { justify-content: center; padding: 10px 0; } + .b31-content { padding: 12px; } + .b31-topbar-tabs { display: none; } +} diff --git a/web-nodejs/public/js/beta31.js b/web-nodejs/public/js/beta31.js new file mode 100644 index 00000000..6af4b72a --- /dev/null +++ b/web-nodejs/public/js/beta31.js @@ -0,0 +1,298 @@ +/** + * BetterDesk Console — Beta 3.1 Panel Controller + * Manages the ISPmanager-inspired shell: sidebar navigation, content loading + */ + +(function () { + 'use strict'; + + const STORAGE_KEY = 'bd_beta31_active'; + const STORAGE_PANEL = 'bd_beta31_panel'; + let shell = null; + let contentArea = null; + let currentPanel = null; + let panelCache = {}; + + /* ────────────────────────────────────────────── + Menu definition — mirrors sidebar.ejs entries + ────────────────────────────────────────────── */ + function getMenuSections() { + const t = (k, fb) => (typeof _ === 'function' ? _(k) : fb) || fb; + return [ + { + heading: t('nav.main', 'Main'), + items: [ + { id: 'dashboard', icon: 'dashboard', label: t('nav.dashboard', 'Dashboard') }, + { id: 'devices', icon: 'devices', label: t('nav.devices', 'Devices') }, + { id: 'registrations', icon: 'how_to_reg', label: t('nav.registrations', 'Registrations') }, + ] + }, + { + heading: t('nav.management', 'Management'), + items: [ + { id: 'inventory', icon: 'inventory_2', label: t('nav.inventory', 'Inventory') }, + { id: 'tickets', icon: 'confirmation_number', label: t('nav.tickets', 'Tickets') }, + { id: 'help-requests', icon: 'support_agent', label: t('nav.help_requests', 'Help Requests') }, + { id: 'automation', icon: 'smart_toy', label: t('nav.automation', 'Automation') }, + { id: 'network', icon: 'lan', label: t('nav.network', 'Network') }, + { id: 'activity', icon: 'timeline', label: t('nav.activity', 'Activity') }, + { id: 'cdap', icon: 'developer_board', label: t('nav.cdap', 'CDAP') }, + { id: 'fleet', icon: 'hub', label: t('nav.fleet', 'Fleet') }, + { id: 'scaling', icon: 'cell_tower', label: t('nav.scaling', 'Scaling') }, + { id: 'cross-platform', icon: 'devices', label: t('nav.cross_platform', 'Cross-Platform') }, + { id: 'chat', icon: 'chat', label: t('nav.chat', 'Chat') }, + ] + }, + { + heading: t('nav.tools', 'Tools'), + items: [ + { id: 'reports', icon: 'assessment', label: t('nav.reports', 'Reports') }, + { id: 'keys', icon: 'vpn_key', label: t('nav.keys', 'Keys') }, + { id: 'generator', icon: 'build', label: t('nav.generator', 'Generator') }, + { id: 'remote', icon: 'connected_tv', label: t('nav.remote', 'Remote Desktop') }, + { id: 'toolkit', icon: 'handyman', label: t('nav.toolkit', 'Toolkit') }, + { id: 'cdap-studio', icon: 'account_tree', label: t('nav.sdk_studio', 'SDK Studio') }, + ] + }, + { + heading: t('nav.system', 'System'), + items: [ + { id: 'tokens', icon: 'token', label: t('nav.tokens', 'Tokens') }, + { id: 'organizations', icon: 'corporate_fare', label: t('nav.organizations', 'Organizations') }, + { id: 'policies', icon: 'policy', label: t('nav.policies', 'Policies') }, + { id: 'attestation', icon: 'fingerprint', label: t('nav.attestation', 'Attestation') }, + { id: 'dataguard', icon: 'shield', label: t('nav.dataguard', 'DataGuard') }, + { id: 'users', icon: 'group', label: t('nav.users', 'Users') }, + { id: 'security-audit', icon: 'security', label: t('nav.security_audit', 'Security Audit') }, + { id: 'permissions', icon: 'admin_panel_settings', label: t('nav.permissions', 'Permissions') }, + { id: 'languages', icon: 'translate', label: t('nav.languages', 'Languages') }, + { id: 'resource-control', icon: 'tune', label: t('nav.resource_control', 'Resource Control') }, + { id: 'settings', icon: 'settings', label: t('nav.settings', 'Settings') }, + ] + } + ]; + } + + /* ────────────────────────────────────────────── + Build the shell DOM + ────────────────────────────────────────────── */ + function buildShell() { + if (document.getElementById('beta31-shell')) return; + + const appName = (window.BetterDesk && window.BetterDesk.branding && window.BetterDesk.branding.appName) || 'BetterDesk'; + const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; + + shell = document.createElement('div'); + shell.id = 'beta31-shell'; + shell.className = 'beta31-shell' + (isDark ? ' b31-dark' : ''); + + // Build topbar + const topbar = document.createElement('div'); + topbar.className = 'b31-topbar'; + topbar.innerHTML = ` + +
+
+ + + +
+ `; + + // Build body (sidebar + content) + const body = document.createElement('div'); + body.className = 'b31-body'; + + // Sidebar + const sidebar = document.createElement('nav'); + sidebar.className = 'b31-sidebar'; + sidebar.id = 'b31-sidebar'; + + const sections = getMenuSections(); + sections.forEach(sec => { + const div = document.createElement('div'); + div.className = 'b31-sidebar-section'; + div.innerHTML = `
${escHtml(sec.heading)}
`; + sec.items.forEach(item => { + const btn = document.createElement('button'); + btn.className = 'b31-sidebar-item'; + btn.dataset.panel = item.id; + btn.innerHTML = `${item.icon}${escHtml(item.label)}`; + btn.addEventListener('click', () => navigateTo(item.id)); + div.appendChild(btn); + }); + sidebar.appendChild(div); + }); + + // Content + contentArea = document.createElement('div'); + contentArea.className = 'b31-content'; + contentArea.id = 'b31-content'; + + body.appendChild(sidebar); + body.appendChild(contentArea); + + shell.appendChild(topbar); + shell.appendChild(body); + document.body.appendChild(shell); + + // Event listeners + document.getElementById('b31-exit-btn').addEventListener('click', deactivate); + document.getElementById('b31-refresh-btn').addEventListener('click', () => { + if (currentPanel) { panelCache[currentPanel] = null; navigateTo(currentPanel); } + }); + document.getElementById('b31-theme-btn').addEventListener('click', toggleTheme); + } + + /* ────────────────────────────────────────────── + Navigation — load panel content via fetch + ────────────────────────────────────────────── */ + function navigateTo(panelId) { + if (!contentArea) return; + currentPanel = panelId; + localStorage.setItem(STORAGE_PANEL, panelId); + + // Update sidebar active state + shell.querySelectorAll('.b31-sidebar-item').forEach(el => { + el.classList.toggle('active', el.dataset.panel === panelId); + }); + + // Show loading + contentArea.innerHTML = '
sync Loading…
'; + + // Use cache if available + if (panelCache[panelId]) { + renderPanel(panelId, panelCache[panelId]); + return; + } + + // Fetch panel content — we load the actual page and extract main-content + fetch('/' + (panelId === 'dashboard' ? '' : panelId), { + headers: { 'X-Beta31': '1', 'X-Requested-With': 'XMLHttpRequest' }, + credentials: 'same-origin' + }) + .then(resp => { + if (!resp.ok) throw new Error('HTTP ' + resp.status); + return resp.text(); + }) + .then(html => { + // Extract inner content from the page + const extracted = extractMainContent(html); + panelCache[panelId] = extracted; + // Only render if still on same panel + if (currentPanel === panelId) renderPanel(panelId, extracted); + }) + .catch(err => { + if (currentPanel === panelId) { + contentArea.innerHTML = `
+

Failed to load panel: ${escHtml(err.message)}

+ +
`; + } + }); + } + + function extractMainContent(html) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const main = doc.querySelector('.main-content'); + if (main) return main.innerHTML; + // Fallback: body content + return doc.body ? doc.body.innerHTML : html; + } + + function renderPanel(panelId, html) { + contentArea.innerHTML = html; + + // Execute inline scripts + contentArea.querySelectorAll('script').forEach(oldScript => { + const newScript = document.createElement('script'); + if (oldScript.src) { + newScript.src = oldScript.src; + } else { + newScript.textContent = oldScript.textContent; + } + oldScript.parentNode.replaceChild(newScript, oldScript); + }); + + // Trigger page-specific init + window.dispatchEvent(new CustomEvent('app:refresh')); + } + + /* ────────────────────────────────────────────── + Activate / Deactivate + ────────────────────────────────────────────── */ + function activate() { + buildShell(); + shell.classList.add('active'); + document.body.style.overflow = 'hidden'; + localStorage.setItem(STORAGE_KEY, '1'); + + // Navigate to saved or default panel + const saved = localStorage.getItem(STORAGE_PANEL) || 'dashboard'; + navigateTo(saved); + } + + function deactivate() { + if (shell) shell.classList.remove('active'); + document.body.style.overflow = ''; + localStorage.setItem(STORAGE_KEY, '0'); + } + + function toggleTheme() { + if (!shell) return; + shell.classList.toggle('b31-dark'); + const icon = document.querySelector('#b31-theme-btn .material-icons'); + if (icon) icon.textContent = shell.classList.contains('b31-dark') ? 'light_mode' : 'dark_mode'; + } + + /* ────────────────────────────────────────────── + Helpers + ────────────────────────────────────────────── */ + function escHtml(s) { + const d = document.createElement('div'); + d.textContent = s || ''; + return d.innerHTML; + } + + /* ────────────────────────────────────────────── + Public API + ────────────────────────────────────────────── */ + window.Beta31 = { + activate: activate, + deactivate: deactivate, + navigateTo: navigateTo, + isActive: () => shell && shell.classList.contains('active'), + init: function () { + // Wire up the toggle button in the navbar + const btn = document.getElementById('beta31-toggle-btn'); + if (btn) btn.addEventListener('click', () => { + if (window.Beta31.isActive()) deactivate(); else activate(); + }); + + // Auto-restore if was active + if (localStorage.getItem(STORAGE_KEY) === '1') { + activate(); + } + } + }; + + // Auto-init on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => window.Beta31.init()); + } else { + window.Beta31.init(); + } +})(); diff --git a/web-nodejs/views/layouts/main.ejs b/web-nodejs/views/layouts/main.ejs index fc758824..a2a54fa3 100644 --- a/web-nodejs/views/layouts/main.ejs +++ b/web-nodejs/views/layouts/main.ejs @@ -19,6 +19,7 @@ + <% } else { %> <% } %> @@ -149,6 +150,7 @@ + <% } %> <% if (typeof pageScripts !== 'undefined' && pageScripts.length) { %> diff --git a/web-nodejs/views/partials/navbar.ejs b/web-nodejs/views/partials/navbar.ejs index 26890977..d9d8f339 100644 --- a/web-nodejs/views/partials/navbar.ejs +++ b/web-nodejs/views/partials/navbar.ejs @@ -50,6 +50,12 @@ + + +