Gitleaks fix

This commit is contained in:
UNITRONIX
2026-04-29 01:54:04 +02:00
parent 33f5e68daf
commit 7ffd109243
9 changed files with 830 additions and 0 deletions
+14
View File
@@ -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$''',
]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+510
View File
@@ -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; }
}
+298
View File
@@ -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 = `
<div class="b31-topbar-logo">
<span class="material-icons">dns</span>
<span>${escHtml(appName)}</span>
<span class="b31-topbar-badge">BETA 3.1</span>
</div>
<div class="b31-topbar-spacer"></div>
<div class="b31-topbar-actions">
<button class="b31-topbar-btn" id="b31-refresh-btn" title="Refresh">
<span class="material-icons">refresh</span>
</button>
<button class="b31-topbar-btn" id="b31-theme-btn" title="Toggle theme">
<span class="material-icons">${isDark ? 'light_mode' : 'dark_mode'}</span>
</button>
<button class="b31-topbar-btn" id="b31-exit-btn" title="Exit Beta 3.1">
<span class="material-icons">close</span>
</button>
</div>
`;
// 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 = `<div class="b31-sidebar-heading">${escHtml(sec.heading)}</div>`;
sec.items.forEach(item => {
const btn = document.createElement('button');
btn.className = 'b31-sidebar-item';
btn.dataset.panel = item.id;
btn.innerHTML = `<span class="material-icons">${item.icon}</span><span>${escHtml(item.label)}</span>`;
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 = '<div class="b31-loading"><span class="material-icons">sync</span> Loading…</div>';
// 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 = `<div class="b31-card"><div class="b31-card-body">
<p style="color:var(--b31-muted)">Failed to load panel: ${escHtml(err.message)}</p>
<button class="b31-btn" onclick="document.getElementById('b31-refresh-btn').click()">
<span class="material-icons">refresh</span> Retry
</button>
</div></div>`;
}
});
}
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();
}
})();
+2
View File
@@ -19,6 +19,7 @@
<link rel="stylesheet" href="/css/desktop-mode.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/desktop-widgets.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/tutorial.css?v=<%= cacheVersion %>">
<link rel="stylesheet" href="/css/beta31.css?v=<%= cacheVersion %>">
<% } else { %>
<link rel="stylesheet" href="/css/desktop-widget-overrides.css?v=<%= cacheVersion %>">
<% } %>
@@ -149,6 +150,7 @@
<script src="/js/desktop-widgets.js?v=<%= cacheVersion %>"></script>
<script src="/js/desktop-mode.js?v=<%= cacheVersion %>"></script>
<script src="/js/tutorial.js?v=<%= cacheVersion %>"></script>
<script src="/js/beta31.js?v=<%= cacheVersion %>"></script>
<script nonce="<%= cspNonce %>">if (typeof Tutorial !== 'undefined') Tutorial.autoStart('console');</script>
<% } %>
<% if (typeof pageScripts !== 'undefined' && pageScripts.length) { %>
+6
View File
@@ -50,6 +50,12 @@
</div>
</div>
<!-- Beta 3.1 Panel toggle -->
<button class="navbar-btn beta31-toggle-btn" id="beta31-toggle-btn" title="Beta 3.1 Console">
<span class="material-icons">view_sidebar</span>
<span class="beta-badge">3.1</span>
</button>
<!-- Desktop mode toggle (Beta) - visible only on large screens -->
<button class="navbar-btn desktop-toggle-btn" id="desktop-toggle-btn" title="<%= _('desktop.switch_mode') %> (Beta)">
<span class="material-icons">desktop_windows</span>