mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 17:45:42 +00:00
cbf72ca1e3
- Added UX 3.5 as the default console shell, featuring a full-list sidebar, topbar, and customizable themes (Dark/Light/Custom). - Updated documentation to reflect the new UX 3.5 shell. - Removed legacy Desktop Mode and Beta 3.1 styles, ensuring a streamlined user experience. - Enhanced language support for new theme options across multiple languages. - Fixed OIDC client login issues by proxying callback URLs to the Go API. This update improves the overall user interface and experience, aligning with modern design standards.
2383 lines
152 KiB
HTML
2383 lines
152 KiB
HTML
<%- include('layouts/main', {
|
|
title: _('nav.settings'),
|
|
pageStyles: ['pages'],
|
|
pageScripts: ['branding-preview', 'settings'],
|
|
currentPage: 'settings',
|
|
breadcrumb: [{ label: _('nav.settings') }],
|
|
body: `
|
|
<div class="page-header">
|
|
<h1 class="page-title">${_('settings.title')}</h1>
|
|
<p class="page-subtitle">${_('settings.subtitle')}</p>
|
|
</div>
|
|
|
|
<div class="settings-shell">
|
|
<nav class="settings-shell-nav mobile-tabs-scroll" role="tablist" aria-label="${_('settings.title')}">
|
|
<button type="button" class="settings-shell-tab settings-tab active" role="tab" aria-selected="true" id="settings-tab-general" data-tab="general" aria-controls="tab-general">
|
|
<span class="material-icons" aria-hidden="true">tune</span>
|
|
${_('settings.tab_general')}
|
|
</button>
|
|
<button type="button" class="settings-shell-tab settings-tab" role="tab" aria-selected="false" id="settings-tab-branding" data-tab="branding" aria-controls="tab-branding">
|
|
<span class="material-icons" aria-hidden="true">palette</span>
|
|
${_('settings.tab_branding')}
|
|
</button>
|
|
${canServerConfig ? `<button type="button" class="settings-shell-tab settings-tab" role="tab" aria-selected="false" id="settings-tab-auth" data-tab="auth" aria-controls="tab-auth">
|
|
<span class="material-icons" aria-hidden="true">admin_panel_settings</span>
|
|
${_('settings.tab_auth')}
|
|
</button>
|
|
<button type="button" class="settings-shell-tab settings-tab" role="tab" aria-selected="false" id="settings-tab-backup" data-tab="backup" aria-controls="tab-backup">
|
|
<span class="material-icons" aria-hidden="true">backup</span>
|
|
${_('settings.tab_backup')}
|
|
</button>
|
|
<button type="button" class="settings-shell-tab settings-tab" role="tab" aria-selected="false" id="settings-tab-updates" data-tab="updates" aria-controls="tab-updates">
|
|
<span class="material-icons" aria-hidden="true">system_update</span>
|
|
${_('updates.tab_title')}
|
|
</button>
|
|
<button type="button" class="settings-shell-tab settings-tab" role="tab" aria-selected="false" id="settings-tab-email" data-tab="email" aria-controls="tab-email">
|
|
<span class="material-icons" aria-hidden="true">email</span>
|
|
${_('settings.tab_email')}
|
|
</button>
|
|
<button type="button" class="settings-shell-tab settings-tab" role="tab" aria-selected="false" id="settings-tab-advanced" data-tab="advanced" aria-controls="tab-advanced">
|
|
<span class="material-icons" aria-hidden="true">code</span>
|
|
${_('settings.tab_advanced')}
|
|
</button>` : ''}
|
|
</nav>
|
|
|
|
<div class="settings-shell-main">
|
|
<div class="settings-search" role="search">
|
|
<span class="material-icons" aria-hidden="true">search</span>
|
|
<input type="search" id="settings-search-input" class="form-input"
|
|
placeholder="${_('settings.ui.search_placeholder')}"
|
|
aria-controls="settings-search-status">
|
|
<button type="button" class="btn btn-ghost btn-sm" id="settings-search-clear" hidden>
|
|
<span class="material-icons">close</span>
|
|
${_('actions.clear')}
|
|
</button>
|
|
</div>
|
|
<div id="settings-search-status" class="settings-search-status" aria-live="polite" hidden></div>
|
|
|
|
<!-- General Tab -->
|
|
<div class="settings-tab-content active" id="tab-general" role="tabpanel" aria-labelledby="settings-tab-general">
|
|
<div class="settings-stack">
|
|
<!-- Password Change Section -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">lock</span>
|
|
${_('settings.change_password')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.password_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form" id="password-form">
|
|
<div class="form-group">
|
|
<label class="form-label" for="current-password">${_('settings.current_password')}</label>
|
|
<input type="password" id="current-password" class="form-input" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="new-password">${_('settings.new_password')}</label>
|
|
<input type="password" id="new-password" class="form-input" required minlength="8">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="confirm-password">${_('settings.confirm_password')}</label>
|
|
<input type="password" id="confirm-password" class="form-input" required>
|
|
</div>
|
|
|
|
<div class="password-requirements">
|
|
<strong>${_('settings.password_requirements')}:</strong>
|
|
<ul>
|
|
<li id="req-length">${_('settings.req_length')}</li>
|
|
<li id="req-uppercase">${_('settings.req_uppercase')}</li>
|
|
<li id="req-lowercase">${_('settings.req_lowercase')}</li>
|
|
<li id="req-number">${_('settings.req_number')}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<button type="submit" class="btn btn-primary">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.save_password')}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Server Information Section -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">info</span>
|
|
${_('settings.server_info')}
|
|
</h2>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="server-info-grid">
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">tag</span>
|
|
${_('settings.version')}
|
|
</span>
|
|
<span class="server-info-value">v${appVersion}</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">dns</span>
|
|
${_('settings.go_server_version')}
|
|
</span>
|
|
<span class="server-info-value" id="go-server-version">-</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">storage</span>
|
|
${_('settings.database')}
|
|
</span>
|
|
<span class="server-info-value" id="db-path">-</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">
|
|
<span class="material-icons">schedule</span>
|
|
${_('settings.uptime')}
|
|
</span>
|
|
<span class="server-info-value" id="uptime">-</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
${canServerConfig ? `
|
|
<div class="settings-panel" id="device-scope-settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">devices</span>
|
|
${_('settings.device_scope_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.device_scope_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form" id="device-scope-form">
|
|
<div class="form-group">
|
|
<label class="form-label">
|
|
<input type="radio" name="device_scope_mode" value="open" id="device-scope-open">
|
|
${_('settings.device_scope_open')}
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">
|
|
<input type="radio" name="device_scope_mode" value="restricted" id="device-scope-restricted">
|
|
${_('settings.device_scope_restricted')}
|
|
</label>
|
|
</div>
|
|
<p class="form-hint" id="device-scope-warning" hidden>${_('settings.device_scope_restricted_warning')}</p>
|
|
<button type="submit" class="btn btn-primary">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.device_scope_save')}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
${canServerConfig ? `
|
|
<div class="settings-panel" id="mesh-settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">hub</span>
|
|
${_('mesh.settings_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('mesh.settings_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="server-info-grid">
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">${_('mesh.enabled')}</span>
|
|
<span class="server-info-value" id="mesh-status-enabled">-</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">${_('mesh.agents_online')}</span>
|
|
<span class="server-info-value" id="mesh-agents-online">-</span>
|
|
</div>
|
|
<div class="server-info-item">
|
|
<span class="server-info-label">${_('mesh.server_id')}</span>
|
|
<span class="server-info-value"><code id="mesh-server-id">-</code></span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" style="margin-top:1rem;">
|
|
<label class="form-label" for="mesh-msh-name">${_('mesh.mesh_name')}</label>
|
|
<input type="text" id="mesh-msh-name" class="form-input" value="BetterDesk Mesh">
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="mesh-copy-server-id">
|
|
<span class="material-icons">content_copy</span>
|
|
${_('mesh.copy_server_id')}
|
|
</button>
|
|
<a class="btn btn-primary" id="mesh-download-msh" href="/api/mesh/download.msh" download="meshagents.msh">
|
|
<span class="material-icons">download</span>
|
|
${_('mesh.download_msh')}
|
|
</a>
|
|
</div>
|
|
<p class="text-muted" style="margin-top:1rem;">${_('mesh.onboarding_body')}</p>
|
|
<div class="alert alert-warning mesh-cert-banner hidden" id="mesh-cert-banner"></div>
|
|
<ol class="mesh-onboarding-steps text-muted">
|
|
<li>${_('mesh.onboarding_step_msh')}</li>
|
|
<li>${_('mesh.onboarding_step_install')}</li>
|
|
<li>${_('mesh.onboarding_step_verify')}</li>
|
|
</ol>
|
|
<div class="settings-subsection" style="margin-top:1.5rem;">
|
|
<h3 class="settings-subsection-title">${_('mesh.groups_title')}</h3>
|
|
<div id="mesh-groups-list" class="mesh-groups-list"></div>
|
|
<div class="form-row" style="margin-top:8px;">
|
|
<input type="text" id="mesh-group-id" class="form-input" placeholder="${_('mesh.group_id_placeholder')}">
|
|
<input type="text" id="mesh-group-name" class="form-input" placeholder="${_('mesh.group_name_placeholder')}">
|
|
<button type="button" class="btn btn-secondary" id="mesh-group-add">${_('mesh.group_add')}</button>
|
|
</div>
|
|
<button type="button" class="btn btn-primary" id="mesh-groups-save" style="margin-top:8px;">${_('mesh.groups_save')}</button>
|
|
</div>
|
|
<div class="settings-subsection" style="margin-top:1.5rem;">
|
|
<h3 class="settings-subsection-title">${_('mesh.recordings_title')}</h3>
|
|
<p class="text-muted">${_('mesh.recordings_desc')}</p>
|
|
<div id="mesh-recordings-table" class="mesh-recordings-table"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Connection Strategy (P2P / Relay) -->
|
|
<div class="settings-panel" id="connection-mode-section">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">swap_horiz</span>
|
|
${_('settings.connection_mode_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.connection_mode_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="alert alert-info" id="connection-mode-info">
|
|
<span class="material-icons">info</span>
|
|
<span>${_('settings.connection_mode_network_hint')}</span>
|
|
</div>
|
|
<div class="settings-choice-grid" role="radiogroup" aria-label="${_('settings.connection_mode_label')}">
|
|
<label class="settings-choice-card">
|
|
<input type="radio" name="connection-mode" value="p2p_first" id="conn-mode-p2p">
|
|
<span class="settings-choice-card-inner">
|
|
<span class="material-icons settings-choice-card-icon">swap_horiz</span>
|
|
<span class="settings-choice-card-title">${_('settings.connection_mode_p2p_first')}</span>
|
|
<span class="settings-choice-card-desc">${_('settings.ui.connection_p2p_hint')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="settings-choice-card">
|
|
<input type="radio" name="connection-mode" value="relay_only" id="conn-mode-relay">
|
|
<span class="settings-choice-card-inner">
|
|
<span class="material-icons settings-choice-card-icon">hub</span>
|
|
<span class="settings-choice-card-title">${_('settings.connection_mode_relay_only')}</span>
|
|
<span class="settings-choice-card-desc">${_('settings.ui.connection_relay_hint')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<details class="connection-mode-advanced">
|
|
<summary>${_('settings.connection_mode_advanced')}</summary>
|
|
<div class="form-row" style="margin-top:1rem;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="conn-p2p-fallback-ms">${_('settings.connection_mode_fallback_ms')}</label>
|
|
<input type="number" id="conn-p2p-fallback-ms" class="form-input" min="0" max="30000" step="100" value="2000">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="conn-same-nat-relay" checked>
|
|
<span>${_('settings.connection_mode_same_nat_relay')}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
<p class="text-muted" id="connection-mode-runtime" hidden></p>
|
|
<p class="text-muted" id="connection-mode-source"></p>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-primary" id="connection-mode-save">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.connection_mode_save')}
|
|
</button>
|
|
<button type="button" class="btn btn-warning" id="connection-mode-save-restart">
|
|
<span class="material-icons">restart_alt</span>
|
|
${_('settings.connection_mode_save_restart')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-panel" id="public-endpoints-section">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">public</span>
|
|
${_('settings.public_endpoints_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.public_endpoints_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="alert alert-info">
|
|
<span class="material-icons">info</span>
|
|
<span>${_('settings.public_endpoints_hint')}</span>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="public-server-id">${_('settings.public_endpoints_server_id')}</label>
|
|
<input type="text" id="public-server-id" class="form-input"
|
|
placeholder="${_('settings.public_endpoints_server_id_placeholder')}" autocomplete="off">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="public-relay-server">${_('settings.public_endpoints_relay')}</label>
|
|
<input type="text" id="public-relay-server" class="form-input"
|
|
placeholder="${_('settings.public_endpoints_relay_placeholder')}" autocomplete="off">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="public-api-url">${_('settings.public_endpoints_api_url')}</label>
|
|
<input type="text" id="public-api-url" class="form-input"
|
|
placeholder="${_('settings.public_endpoints_api_url_placeholder')}" autocomplete="off">
|
|
<p class="text-muted form-hint">${_('settings.public_endpoints_api_url_hint')}</p>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-primary" id="public-endpoints-save">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.public_endpoints_save')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
|
|
<!-- Two-Factor Authentication Section -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">security</span>
|
|
${_('settings.totp_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.totp_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div id="totp-status-container">
|
|
<div class="totp-status-loading">
|
|
<span class="material-icons spin">sync</span>
|
|
${_('common.loading')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Log Section -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">history</span>
|
|
${_('settings.audit_log')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.audit_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="audit-log-container">
|
|
<table class="audit-log-table">
|
|
<thead>
|
|
<tr>
|
|
<th>${_('audit.time')}</th>
|
|
<th>${_('audit.user')}</th>
|
|
<th>${_('audit.action')}</th>
|
|
<th>${_('audit.details')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="audit-log-body">
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted">
|
|
${_('common.loading')}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tutorials Section -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">school</span>
|
|
${_('settings.tutorials_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.tutorials_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">
|
|
<label class="form-label" for="tutorials-enabled">${_('settings.tutorials_enabled')}</label>
|
|
<p class="settings-row-hint">${_('settings.tutorials_enabled_hint')}</p>
|
|
</div>
|
|
<div class="settings-row-control">
|
|
<input type="checkbox" id="tutorials-enabled" class="toggle-switch" aria-describedby="tutorials-enabled-hint">
|
|
</div>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">
|
|
<span class="form-label">${_('tutorial.reset_all')}</span>
|
|
<p class="settings-row-hint" id="tutorials-reset-hint">${_('settings.tutorials_reset_hint')}</p>
|
|
</div>
|
|
<div class="settings-row-control">
|
|
<button type="button" class="btn btn-secondary btn-sm" id="tutorials-reset-btn">
|
|
<span class="material-icons">refresh</span>
|
|
${_('tutorial.reset_all')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Branding Tab -->
|
|
<div class="settings-tab-content" id="tab-branding" role="tabpanel" aria-labelledby="settings-tab-branding" data-can-branding-edit="${canBrandingEdit ? '1' : '0'}">
|
|
<div class="branding-studio">
|
|
<section class="branding-panel-hero branding-hero">
|
|
<div class="branding-hero-copy">
|
|
<span class="branding-panel-hero-kicker branding-hero-kicker">${_('settings.tab_branding')}</span>
|
|
<h2>${_('branding.live_preview')}</h2>
|
|
<p>${_('branding.background_desc')}</p>
|
|
<div class="branding-panel-hero-steps branding-hero-steps">
|
|
<span><span class="material-icons" aria-hidden="true">palette</span>${_('branding.module_colors')}</span>
|
|
<span><span class="material-icons" aria-hidden="true">wallpaper</span>${_('branding.module_backgrounds')}</span>
|
|
<span><span class="material-icons" aria-hidden="true">connected_tv</span>${_('settings.ui.branding_rdclient')}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<div class="branding-studio-toolbar">
|
|
<div class="branding-save-status" id="branding-save-status" aria-live="polite">
|
|
<span class="material-icons branding-status-icon">check_circle</span>
|
|
<span id="branding-status-text">${_('branding.status_saved')}</span>
|
|
</div>
|
|
<div class="branding-toolbar-actions">
|
|
<button type="button" class="btn btn-ghost btn-sm" id="branding-revert-btn" title="${_('branding.revert_changes')}">
|
|
<span class="material-icons">undo</span>
|
|
${_('branding.revert_changes')}
|
|
</button>
|
|
<button type="button" class="btn btn-primary btn-sm" id="branding-save-btn">
|
|
<span class="material-icons">save</span>
|
|
${_('branding.save_now')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="branding-readability-alert" id="branding-readability-alert" hidden>
|
|
<span class="material-icons">visibility</span>
|
|
<div>
|
|
<strong>Readability check</strong>
|
|
<p id="branding-readability-text"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="branding-profiles-bar">
|
|
<div class="branding-profiles-main">
|
|
<label class="form-label" for="branding-profile-select">${_('branding.profiles_label')}</label>
|
|
<select id="branding-profile-select" class="form-input form-select branding-profile-select"></select>
|
|
<button type="button" class="btn btn-secondary btn-sm" id="branding-profile-apply-btn">
|
|
<span class="material-icons">play_arrow</span>
|
|
${_('branding.profile_apply')}
|
|
</button>
|
|
</div>
|
|
<div class="branding-profiles-actions">
|
|
<button type="button" class="btn btn-ghost btn-sm" id="branding-profile-new-btn">
|
|
<span class="material-icons">add</span>
|
|
${_('branding.profile_new')}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm" id="branding-profile-save-btn">
|
|
<span class="material-icons">save_as</span>
|
|
${_('branding.profile_save')}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm" id="branding-profile-duplicate-btn">
|
|
<span class="material-icons">content_copy</span>
|
|
${_('branding.profile_duplicate')}
|
|
</button>
|
|
<button type="button" class="btn btn-ghost btn-sm btn-danger-text" id="branding-profile-delete-btn">
|
|
<span class="material-icons">delete</span>
|
|
${_('branding.profile_delete')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="branding-builtin-themes" id="branding-builtin-themes">
|
|
<h3 class="branding-builtin-title">${_('branding.builtin_themes')}</h3>
|
|
<div class="branding-theme-gallery" id="branding-theme-gallery"></div>
|
|
</div>
|
|
|
|
<div class="branding-studio-body">
|
|
<nav class="branding-module-nav" id="branding-module-nav" aria-label="${_('branding.modules_nav')}">
|
|
<button type="button" class="branding-module-btn active" data-branding-module="identity">
|
|
<span class="material-icons">badge</span>
|
|
${_('branding.module_identity')}
|
|
</button>
|
|
<button type="button" class="branding-module-btn" data-branding-module="colors">
|
|
<span class="material-icons">palette</span>
|
|
${_('branding.module_colors')}
|
|
</button>
|
|
<button type="button" class="branding-module-btn" data-branding-module="typography">
|
|
<span class="material-icons">text_format</span>
|
|
${_('branding.module_typography')}
|
|
</button>
|
|
<button type="button" class="branding-module-btn" data-branding-module="backgrounds">
|
|
<span class="material-icons">wallpaper</span>
|
|
${_('branding.module_backgrounds')}
|
|
</button>
|
|
<button type="button" class="branding-module-btn" data-branding-module="pages">
|
|
<span class="material-icons">web</span>
|
|
${_('branding.module_pages')}
|
|
</button>
|
|
<button type="button" class="branding-module-btn" data-branding-module="advanced">
|
|
<span class="material-icons">tune</span>
|
|
${_('branding.module_advanced')}
|
|
</button>
|
|
</nav>
|
|
|
|
<div class="settings-stack branding-modules" id="branding-modules">
|
|
<!-- Brand Identity -->
|
|
<div class="settings-panel branding-module-panel active" data-branding-module="identity">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">badge</span>
|
|
${_('branding.identity_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.identity_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form branding-form" id="branding-identity-form" style="max-width: 600px;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="brand-name">${_('branding.app_name')}</label>
|
|
<input type="text" id="brand-name" class="form-input" maxlength="50"
|
|
placeholder="BetterDesk">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="brand-description">${_('branding.app_description')}</label>
|
|
<input type="text" id="brand-description" class="form-input" maxlength="100"
|
|
placeholder="BetterDesk Server Management">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logo Configuration -->
|
|
<div class="settings-panel branding-module-panel active" data-branding-module="identity">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">image</span>
|
|
${_('branding.logo_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.logo_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="branding-logo-config">
|
|
<div class="logo-type-selector">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="text">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">title</span>
|
|
<span>${_('branding.logo_type_text')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="icon">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">text_fields</span>
|
|
<span>${_('branding.logo_type_icon')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="svg">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">code</span>
|
|
<span>${_('branding.logo_type_svg')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="logo-type" value="image">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">photo</span>
|
|
<span>${_('branding.logo_type_image')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Text Logo options -->
|
|
<div class="logo-config-panel hidden" id="logo-text-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-text-input">${_('branding.logo_text')}</label>
|
|
<input type="text" id="logo-text-input" class="form-input"
|
|
placeholder="insolve" maxlength="30">
|
|
<small class="form-hint">${_('branding.logo_text_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-text-accent">${_('branding.logo_text_accent')}</label>
|
|
<input type="text" id="logo-text-accent" class="form-input"
|
|
placeholder="Core Server" maxlength="30">
|
|
<small class="form-hint">${_('branding.logo_text_accent_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Icon options -->
|
|
<div class="logo-config-panel" id="logo-icon-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-icon-name">${_('branding.logo_icon_name')}</label>
|
|
<input type="text" id="logo-icon-name" class="form-input"
|
|
placeholder="dns" maxlength="50">
|
|
<small class="form-hint">${_('branding.logo_icon_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- SVG options -->
|
|
<div class="logo-config-panel hidden" id="logo-svg-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-svg-input">${_('branding.logo_svg_code')}</label>
|
|
<textarea id="logo-svg-input" class="form-input form-textarea" rows="6"
|
|
placeholder="<svg>...</svg>"></textarea>
|
|
<small class="form-hint">${_('branding.logo_svg_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Image options -->
|
|
<div class="logo-config-panel hidden" id="logo-image-panel">
|
|
<div class="form-group">
|
|
<label class="form-label">${_('branding.logo_image_upload')}</label>
|
|
<div class="logo-upload-area">
|
|
<input type="file" id="logo-image-file" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml" class="logo-file-input">
|
|
<label for="logo-image-file" class="logo-upload-btn">
|
|
<span class="material-icons">upload_file</span>
|
|
<span>${_('branding.logo_image_choose')}</span>
|
|
</label>
|
|
<span id="logo-file-name" class="logo-file-name"></span>
|
|
</div>
|
|
<small class="form-hint">${_('branding.logo_image_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="logo-image-url">${_('branding.logo_image_url')}</label>
|
|
<input type="text" id="logo-image-url" class="form-input"
|
|
placeholder="https://example.com/logo.png">
|
|
<small class="form-hint">${_('branding.logo_image_url_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logo preview -->
|
|
<div class="branding-preview-box">
|
|
<label class="form-label">${_('branding.preview')}</label>
|
|
<div class="logo-preview" id="logo-preview">
|
|
<span class="material-icons">dns</span>
|
|
<span class="logo-preview-text">BetterDesk</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Color Scheme -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="colors">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">palette</span>
|
|
${_('branding.colors_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.colors_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="form-group" style="max-width: 520px; margin-bottom: 1.25rem;">
|
|
<label class="form-label" for="theme-mode-select">${_('theme.title') || 'Theme'}</label>
|
|
<p class="settings-row-hint">${_('ux35.theme_mode_hint') || 'Light and Dark are built-in. Custom activates when you edit colors below.'}</p>
|
|
<div class="logo-type-selector" id="theme-mode-selector" role="radiogroup" aria-label="${_('theme.title') || 'Theme'}">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="theme-mode" value="dark" checked>
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">dark_mode</span>
|
|
<span>${_('theme.dark') || 'Dark'}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="theme-mode" value="light">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">light_mode</span>
|
|
<span>${_('theme.light') || 'Light'}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="theme-mode" value="custom">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">palette</span>
|
|
<span>${_('theme.custom') || 'Custom'}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="color-grid">
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_backgrounds')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Primary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgPrimary" value="#0d1117">
|
|
<input type="text" class="form-input color-hex" data-color="bgPrimary"
|
|
placeholder="#0d1117" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Secondary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgSecondary" value="#161b22">
|
|
<input type="text" class="form-input color-hex" data-color="bgSecondary"
|
|
placeholder="#161b22" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Tertiary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgTertiary" value="#21262d">
|
|
<input type="text" class="form-input color-hex" data-color="bgTertiary"
|
|
placeholder="#21262d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Elevated</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="bgElevated" value="#30363d">
|
|
<input type="text" class="form-input color-hex" data-color="bgElevated"
|
|
placeholder="#30363d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_text')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Primary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="textPrimary" value="#e6edf3">
|
|
<input type="text" class="form-input color-hex" data-color="textPrimary"
|
|
placeholder="#e6edf3" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Secondary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="textSecondary" value="#8b949e">
|
|
<input type="text" class="form-input color-hex" data-color="textSecondary"
|
|
placeholder="#8b949e" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_accents')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Blue (Primary Accent)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentBlue" value="#58a6ff">
|
|
<input type="text" class="form-input color-hex" data-color="accentBlue"
|
|
placeholder="#58a6ff" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Green (Success)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentGreen" value="#2ea44f">
|
|
<input type="text" class="form-input color-hex" data-color="accentGreen"
|
|
placeholder="#2ea44f" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Red (Danger)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentRed" value="#f85149">
|
|
<input type="text" class="form-input color-hex" data-color="accentRed"
|
|
placeholder="#f85149" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Yellow (Warning)</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentYellow" value="#d29922">
|
|
<input type="text" class="form-input color-hex" data-color="accentYellow"
|
|
placeholder="#d29922" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Purple</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="accentPurple" value="#a371f7">
|
|
<input type="text" class="form-input color-hex" data-color="accentPurple"
|
|
placeholder="#a371f7" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="color-group">
|
|
<h3 class="color-group-title">${_('branding.colors_borders')}</h3>
|
|
<div class="color-row">
|
|
<label class="form-label">Primary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="borderPrimary" value="#30363d">
|
|
<input type="text" class="form-input color-hex" data-color="borderPrimary"
|
|
placeholder="#30363d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
<div class="color-row">
|
|
<label class="form-label">Secondary</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" data-color="borderSecondary" value="#21262d">
|
|
<input type="text" class="form-input color-hex" data-color="borderSecondary"
|
|
placeholder="#21262d" maxlength="7">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Typography -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="typography">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">text_format</span>
|
|
${_('branding.typography_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.typography_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="font-config">
|
|
<!-- Heading Font -->
|
|
<div class="font-slot" id="font-heading-slot">
|
|
<div class="font-slot-header">
|
|
<h3 class="font-slot-title">${_('branding.font_heading')}</h3>
|
|
<small class="form-hint">${_('branding.font_heading_hint')}</small>
|
|
</div>
|
|
<div class="font-picker-row">
|
|
<div class="font-search-wrapper">
|
|
<input type="text" id="font-heading-search" class="form-input font-search-input"
|
|
placeholder="${_('branding.font_search_placeholder')}" autocomplete="off">
|
|
<div class="font-dropdown hidden" id="font-heading-dropdown"></div>
|
|
</div>
|
|
<input type="hidden" id="font-heading-value">
|
|
<span class="font-current" id="font-heading-current">${_('branding.font_system_default')}</span>
|
|
<button class="btn btn-ghost btn-sm font-clear-btn hidden" id="font-heading-clear" title="${_('branding.font_clear')}">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="font-preview" id="font-heading-preview">
|
|
<span class="font-preview-text" style="font-size: 1.5rem; font-weight: 600;">The quick brown fox jumps over the lazy dog</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Body Font -->
|
|
<div class="font-slot" id="font-body-slot">
|
|
<div class="font-slot-header">
|
|
<h3 class="font-slot-title">${_('branding.font_body')}</h3>
|
|
<small class="form-hint">${_('branding.font_body_hint')}</small>
|
|
</div>
|
|
<div class="font-picker-row">
|
|
<div class="font-search-wrapper">
|
|
<input type="text" id="font-body-search" class="form-input font-search-input"
|
|
placeholder="${_('branding.font_search_placeholder')}" autocomplete="off">
|
|
<div class="font-dropdown hidden" id="font-body-dropdown"></div>
|
|
</div>
|
|
<input type="hidden" id="font-body-value">
|
|
<span class="font-current" id="font-body-current">${_('branding.font_system_default')}</span>
|
|
<button class="btn btn-ghost btn-sm font-clear-btn hidden" id="font-body-clear" title="${_('branding.font_clear')}">
|
|
<span class="material-icons">close</span>
|
|
</button>
|
|
</div>
|
|
<div class="font-preview" id="font-body-preview">
|
|
<span class="font-preview-text">The quick brown fox jumps over the lazy dog. 0123456789</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Font source tabs -->
|
|
<div class="font-source-tabs" role="tablist">
|
|
<button type="button" class="font-source-tab active" data-font-source="google">${_('branding.font_google')}</button>
|
|
<button type="button" class="font-source-tab" data-font-source="local">${_('branding.font_local')}</button>
|
|
<button type="button" class="font-source-tab" data-font-source="upload">${_('branding.font_upload')}</button>
|
|
</div>
|
|
<div class="font-source-panel active" id="font-source-google"></div>
|
|
<div class="font-source-panel" id="font-source-local">
|
|
<div class="font-local-list" id="font-local-list"></div>
|
|
</div>
|
|
<div class="font-source-panel" id="font-source-upload">
|
|
<div class="font-upload-area">
|
|
<input type="file" id="font-upload-file" accept=".woff2,.ttf,font/woff2,font/ttf" class="font-upload-input">
|
|
<label for="font-upload-file" class="btn btn-secondary btn-sm">
|
|
<span class="material-icons">upload_file</span>
|
|
${_('branding.font_upload_choose')}
|
|
</label>
|
|
<input type="text" id="font-upload-name" class="form-input" placeholder="${_('branding.font_upload_name')}" maxlength="80">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category filter -->
|
|
<div class="font-category-filter">
|
|
<button class="btn btn-ghost btn-sm font-cat-btn active" data-category="all">${_('branding.font_cat_all')}</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="sans-serif">Sans-Serif</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="serif">Serif</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="monospace">Mono</button>
|
|
<button class="btn btn-ghost btn-sm font-cat-btn" data-category="display">Display</button>
|
|
</div>
|
|
|
|
<!-- Downloaded fonts info -->
|
|
<div class="font-local-info" id="font-local-info">
|
|
<span class="material-icons">folder</span>
|
|
<span id="font-local-count">0</span> ${_('branding.font_downloaded_count')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Background & Appearance -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="backgrounds">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">wallpaper</span>
|
|
${_('branding.background_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.background_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="branding-bg-config">
|
|
<div class="logo-type-selector">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="bg-type" value="none">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">block</span>
|
|
<span>${_('branding.bg_type_none')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="bg-type" value="color">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">format_color_fill</span>
|
|
<span>${_('branding.bg_type_color')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="bg-type" value="gradient">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">gradient</span>
|
|
<span>${_('branding.bg_type_gradient')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="bg-type" value="image">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">image</span>
|
|
<span>${_('branding.bg_type_image')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="bg-color-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="bg-color">${_('branding.bg_color')}</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" id="bg-color-picker" value="#0d1117">
|
|
<input type="text" class="form-input color-hex" id="bg-color"
|
|
placeholder="#0d1117" maxlength="32">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="bg-gradient-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="bg-gradient">${_('branding.bg_gradient')}</label>
|
|
<input type="text" id="bg-gradient" class="form-input"
|
|
placeholder="linear-gradient(135deg, #1e293b 0%, #0f172a 100%)" maxlength="400">
|
|
<small class="form-hint">${_('branding.bg_gradient_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="bg-image-panel">
|
|
<div class="background-picker">
|
|
<div class="background-picker-head">
|
|
<div>
|
|
<label class="form-label">${_('desktop.wp_images')}</label>
|
|
<small class="form-hint">${_('branding.bg_image_hint')}</small>
|
|
</div>
|
|
<input type="file" id="bg-image-file" data-target="bg-image-url" accept="image/png,image/jpeg,image/webp,image/gif" class="logo-file-input branding-bg-file">
|
|
<label for="bg-image-file" class="logo-upload-btn">
|
|
<span class="material-icons">upload_file</span>
|
|
<span>${_('branding.bg_image_choose')}</span>
|
|
</label>
|
|
</div>
|
|
<div class="background-library" id="bg-image-library" aria-label="${_('desktop.wp_images')}"></div>
|
|
<span id="bg-file-name" class="logo-file-name background-current-file"></span>
|
|
<div class="branding-upload-status" id="bg-upload-status" hidden>
|
|
<div class="branding-upload-status-row">
|
|
<span class="material-icons branding-upload-status-icon">sync</span>
|
|
<span class="branding-upload-message"></span>
|
|
</div>
|
|
<div class="branding-upload-progress">
|
|
<div class="branding-upload-progress-fill"></div>
|
|
</div>
|
|
</div>
|
|
<details class="background-advanced-url">
|
|
<summary>${_('settings.tab_advanced')}</summary>
|
|
<div class="form-group">
|
|
<label class="form-label" for="bg-image-url">${_('branding.bg_image_url')}</label>
|
|
<input type="text" id="bg-image-url" class="form-input"
|
|
placeholder="https://example.com/wallpaper.jpg">
|
|
<small class="form-hint">${_('branding.bg_image_hint')}</small>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="bg-size">${_('branding.bg_size')}</label>
|
|
<select id="bg-size" class="form-input form-select">
|
|
<option value="cover">${_('branding.bg_size_cover')}</option>
|
|
<option value="contain">${_('branding.bg_size_contain')}</option>
|
|
<option value="auto">${_('branding.bg_size_auto')}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" id="bg-effects-group">
|
|
<label class="form-label" for="bg-blur">${_('branding.bg_blur')} (<span id="bg-blur-value">0</span>px)</label>
|
|
<input type="range" id="bg-blur" class="form-range" min="0" max="30" step="1" value="0">
|
|
</div>
|
|
<div class="form-group" id="bg-overlay-group">
|
|
<label class="form-label" for="bg-overlay">${_('branding.bg_overlay')} (<span id="bg-overlay-value">0</span>%)</label>
|
|
<input type="range" id="bg-overlay" class="form-range" min="0" max="90" step="5" value="0">
|
|
<small class="form-hint">${_('branding.bg_overlay_hint')}</small>
|
|
</div>
|
|
|
|
<div class="branding-glass-config">
|
|
<h3 class="branding-subsection-title">${_('branding.glass_title')}</h3>
|
|
<p class="form-hint branding-subsection-desc">${_('branding.glass_desc')}</p>
|
|
<div class="form-group">
|
|
<label class="form-check">
|
|
<input type="checkbox" id="glass-enabled" checked>
|
|
<span>${_('branding.glass_enabled')}</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="glass-color">${_('branding.glass_color')}</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" id="glass-color-picker" value="#161b22">
|
|
<input type="text" class="form-input color-hex" id="glass-color"
|
|
placeholder="#161b22" maxlength="7">
|
|
</div>
|
|
<small class="form-hint">${_('branding.glass_color_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="glass-blur">${_('branding.glass_blur')} (<span id="glass-blur-value">16</span>px)</label>
|
|
<input type="range" id="glass-blur" class="form-range" min="0" max="40" step="1" value="16">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="glass-opacity">${_('branding.glass_opacity')} (<span id="glass-opacity-value">55</span>%)</label>
|
|
<input type="range" id="glass-opacity" class="form-range" min="0" max="100" step="5" value="55">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Login Page -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="pages">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">login</span>
|
|
${_('branding.login_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.login_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form branding-form" style="max-width: 600px;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="login-title">${_('branding.login_heading')}</label>
|
|
<input type="text" id="login-title" class="form-input" maxlength="80"
|
|
placeholder="${_('branding.login_heading_placeholder')}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="login-subtitle">${_('branding.login_subheading')}</label>
|
|
<input type="text" id="login-subtitle" class="form-input" maxlength="160"
|
|
placeholder="${_('branding.login_subheading_placeholder')}">
|
|
</div>
|
|
</form>
|
|
<div class="branding-bg-config" style="margin-top: 16px;">
|
|
<div class="logo-type-selector">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="login-bg-type" value="inherit">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">sync</span>
|
|
<span>${_('branding.login_bg_inherit')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="login-bg-type" value="color">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">format_color_fill</span>
|
|
<span>${_('branding.bg_type_color')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="login-bg-type" value="gradient">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">gradient</span>
|
|
<span>${_('branding.bg_type_gradient')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="login-bg-type" value="image">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">image</span>
|
|
<span>${_('branding.bg_type_image')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="login-bg-color-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="login-bg-color">${_('branding.bg_color')}</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" id="login-bg-color-picker" value="#0d1117">
|
|
<input type="text" class="form-input color-hex" id="login-bg-color"
|
|
placeholder="#0d1117" maxlength="32">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="login-bg-gradient-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="login-bg-gradient">${_('branding.bg_gradient')}</label>
|
|
<input type="text" id="login-bg-gradient" class="form-input"
|
|
placeholder="linear-gradient(135deg, #1e293b 0%, #0f172a 100%)" maxlength="400">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="login-bg-image-panel">
|
|
<div class="form-group">
|
|
<label class="form-label">${_('branding.bg_image_upload')}</label>
|
|
<div class="logo-upload-area">
|
|
<input type="file" id="login-bg-image-file" data-target="login-bg-image-url" accept="image/png,image/jpeg,image/webp,image/gif" class="logo-file-input branding-bg-file">
|
|
<label for="login-bg-image-file" class="logo-upload-btn">
|
|
<span class="material-icons">upload_file</span>
|
|
<span>${_('branding.bg_image_choose')}</span>
|
|
</label>
|
|
<span id="login-bg-file-name" class="logo-file-name"></span>
|
|
</div>
|
|
<div class="branding-upload-status" id="login-bg-upload-status" hidden>
|
|
<div class="branding-upload-status-row">
|
|
<span class="material-icons branding-upload-status-icon">sync</span>
|
|
<span class="branding-upload-message"></span>
|
|
</div>
|
|
<div class="branding-upload-progress">
|
|
<div class="branding-upload-progress-fill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="login-bg-image-url">${_('branding.bg_image_url')}</label>
|
|
<input type="text" id="login-bg-image-url" class="form-input"
|
|
placeholder="https://example.com/login-bg.jpg">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="login-bg-overlay">${_('branding.bg_overlay')} (<span id="login-bg-overlay-value">0</span>%)</label>
|
|
<input type="range" id="login-bg-overlay" class="form-range" min="0" max="90" step="5" value="0">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agent Download Page -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="pages">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">download</span>
|
|
${_('branding.agent_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.agent_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="branding-bg-config">
|
|
<div class="logo-type-selector">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="agent-bg-type" value="none">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">block</span>
|
|
<span>${_('branding.bg_type_none')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="agent-bg-type" value="color">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">format_color_fill</span>
|
|
<span>${_('branding.bg_type_color')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="agent-bg-type" value="gradient">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">gradient</span>
|
|
<span>${_('branding.bg_type_gradient')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="agent-bg-type" value="image">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">image</span>
|
|
<span>${_('branding.bg_type_image')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="agent-bg-color-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="agent-bg-color">${_('branding.bg_color')}</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" id="agent-bg-color-picker" value="#f1f5f9">
|
|
<input type="text" class="form-input color-hex" id="agent-bg-color"
|
|
placeholder="#f1f5f9" maxlength="32">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="agent-bg-gradient-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="agent-bg-gradient">${_('branding.bg_gradient')}</label>
|
|
<input type="text" id="agent-bg-gradient" class="form-input"
|
|
placeholder="linear-gradient(135deg, #eef2ff 0%, #e0e7ff 100%)" maxlength="400">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="agent-bg-image-panel">
|
|
<div class="form-group">
|
|
<label class="form-label">${_('branding.bg_image_upload')}</label>
|
|
<div class="logo-upload-area">
|
|
<input type="file" id="agent-bg-image-file" data-target="agent-bg-image-url" accept="image/png,image/jpeg,image/webp,image/gif" class="logo-file-input branding-bg-file">
|
|
<label for="agent-bg-image-file" class="logo-upload-btn">
|
|
<span class="material-icons">upload_file</span>
|
|
<span>${_('branding.bg_image_choose')}</span>
|
|
</label>
|
|
<span id="agent-bg-file-name" class="logo-file-name"></span>
|
|
</div>
|
|
<div class="branding-upload-status" id="agent-bg-upload-status" hidden>
|
|
<div class="branding-upload-status-row">
|
|
<span class="material-icons branding-upload-status-icon">sync</span>
|
|
<span class="branding-upload-message"></span>
|
|
</div>
|
|
<div class="branding-upload-progress">
|
|
<div class="branding-upload-progress-fill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="agent-bg-image-url">${_('branding.bg_image_url')}</label>
|
|
<input type="text" id="agent-bg-image-url" class="form-input"
|
|
placeholder="https://example.com/agent-bg.jpg">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group toggle-group" style="display:flex;align-items:center;gap:12px;margin-top:8px;">
|
|
<input type="checkbox" id="agent-show-powered" class="toggle-switch" checked>
|
|
<div>
|
|
<label class="form-label" for="agent-show-powered" style="margin-bottom:0;cursor:pointer;">${_('branding.agent_show_powered')}</label>
|
|
<small class="form-hint" style="display:block;margin-top:2px;">${_('branding.agent_show_powered_hint')}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RdClient / Remote Dashboard -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="pages">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">connected_tv</span>
|
|
RdClient
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('remote_dashboard.subtitle')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="branding-bg-config">
|
|
<div class="logo-type-selector">
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="rdclient-bg-type" value="inherit">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">sync</span>
|
|
<span>${_('branding.login_bg_inherit')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="rdclient-bg-type" value="none">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">block</span>
|
|
<span>${_('branding.bg_type_none')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="rdclient-bg-type" value="color">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">format_color_fill</span>
|
|
<span>${_('branding.bg_type_color')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="rdclient-bg-type" value="gradient">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">gradient</span>
|
|
<span>${_('branding.bg_type_gradient')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="logo-type-option">
|
|
<input type="radio" name="rdclient-bg-type" value="image">
|
|
<span class="logo-type-card">
|
|
<span class="material-icons">image</span>
|
|
<span>${_('branding.bg_type_image')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="rdclient-bg-color-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="rdclient-bg-color">${_('branding.bg_color')}</label>
|
|
<div class="color-input-wrapper">
|
|
<input type="color" class="color-picker" id="rdclient-bg-color-picker" value="#0d1117">
|
|
<input type="text" class="form-input color-hex" id="rdclient-bg-color"
|
|
placeholder="#0d1117" maxlength="32">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="rdclient-bg-gradient-panel">
|
|
<div class="form-group">
|
|
<label class="form-label" for="rdclient-bg-gradient">${_('branding.bg_gradient')}</label>
|
|
<input type="text" id="rdclient-bg-gradient" class="form-input"
|
|
placeholder="linear-gradient(135deg, #0d1117 0%, #172554 100%)" maxlength="400">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="logo-config-panel hidden" id="rdclient-bg-image-panel">
|
|
<div class="form-group">
|
|
<label class="form-label">${_('branding.bg_image_upload')}</label>
|
|
<div class="logo-upload-area">
|
|
<input type="file" id="rdclient-bg-image-file" data-target="rdclient-bg-image-url" accept="image/png,image/jpeg,image/webp,image/gif" class="logo-file-input branding-bg-file">
|
|
<label for="rdclient-bg-image-file" class="logo-upload-btn">
|
|
<span class="material-icons">upload_file</span>
|
|
<span>${_('branding.bg_image_choose')}</span>
|
|
</label>
|
|
<span id="rdclient-bg-file-name" class="logo-file-name"></span>
|
|
</div>
|
|
<div class="branding-upload-status" id="rdclient-bg-upload-status" hidden>
|
|
<div class="branding-upload-status-row">
|
|
<span class="material-icons branding-upload-status-icon">sync</span>
|
|
<span class="branding-upload-message"></span>
|
|
</div>
|
|
<div class="branding-upload-progress">
|
|
<div class="branding-upload-progress-fill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="rdclient-bg-image-url">${_('branding.bg_image_url')}</label>
|
|
<input type="text" id="rdclient-bg-image-url" class="form-input"
|
|
placeholder="https://example.com/rdclient-bg.jpg">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="rdclient-bg-size">${_('branding.bg_size')}</label>
|
|
<select id="rdclient-bg-size" class="form-input form-select">
|
|
<option value="cover">${_('branding.bg_size_cover')}</option>
|
|
<option value="contain">${_('branding.bg_size_contain')}</option>
|
|
<option value="auto">${_('branding.bg_size_auto')}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="rdclient-bg-overlay">${_('branding.bg_overlay')} (<span id="rdclient-bg-overlay-value">0</span>%)</label>
|
|
<input type="range" id="rdclient-bg-overlay" class="form-range" min="0" max="90" step="5" value="0">
|
|
<small class="form-hint">${_('branding.bg_overlay_hint')}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer & Copyright -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="advanced">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">copyright</span>
|
|
${_('branding.footer_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.footer_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form branding-form" style="max-width: 600px;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="footer-text">${_('branding.footer_text')}</label>
|
|
<input type="text" id="footer-text" class="form-input" maxlength="200"
|
|
placeholder="© 2026 Your Company. All rights reserved.">
|
|
<small class="form-hint">${_('branding.footer_text_hint')}</small>
|
|
</div>
|
|
<div class="form-group toggle-group" style="display:flex;align-items:center;gap:12px;">
|
|
<input type="checkbox" id="show-powered" class="toggle-switch" checked>
|
|
<div>
|
|
<label class="form-label" for="show-powered" style="margin-bottom:0;cursor:pointer;">${_('branding.show_powered')}</label>
|
|
<small class="form-hint" style="display:block;margin-top:2px;">${_('branding.show_powered_hint')}</small>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom CSS -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="advanced">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">code</span>
|
|
${_('branding.custom_css_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.custom_css_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="form-group" style="max-width: 800px;">
|
|
<textarea id="custom-css" class="form-input form-textarea code-textarea" rows="10"
|
|
placeholder=".sidebar { box-shadow: 0 0 20px rgba(0,0,0,.4); }"></textarea>
|
|
<small class="form-hint">${_('branding.custom_css_hint')}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Import / Export -->
|
|
<div class="settings-panel branding-module-panel" data-branding-module="advanced">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">swap_horiz</span>
|
|
${_('branding.import_export_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('branding.import_export_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="branding-actions-row">
|
|
<button class="btn btn-secondary" id="branding-export-btn">
|
|
<span class="material-icons">download</span>
|
|
${_('branding.export_theme')}
|
|
</button>
|
|
<label class="btn btn-secondary">
|
|
<span class="material-icons">upload</span>
|
|
${_('branding.import_theme')}
|
|
<input type="file" id="branding-import-input" accept=".json" hidden>
|
|
</label>
|
|
<button class="btn btn-danger" id="branding-reset-btn">
|
|
<span class="material-icons">restart_alt</span>
|
|
${_('branding.reset_defaults')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<aside class="branding-live-preview" id="branding-live-preview">
|
|
<div class="branding-live-preview-header">
|
|
<h3>${_('branding.live_preview')}</h3>
|
|
<label class="branding-preview-page-toggle">
|
|
<input type="checkbox" id="branding-preview-page-toggle">
|
|
<span>${_('branding.preview_apply_page')}</span>
|
|
</label>
|
|
</div>
|
|
<div class="branding-preview-mockup" id="branding-live-mockup">
|
|
<div class="branding-mockup-sidebar">
|
|
<div class="branding-mockup-logo" id="branding-mockup-logo">
|
|
<span class="material-icons">dns</span>
|
|
<span class="branding-mockup-title">BetterDesk</span>
|
|
</div>
|
|
<div class="branding-mockup-nav-item active"></div>
|
|
<div class="branding-mockup-nav-item"></div>
|
|
<div class="branding-mockup-nav-item"></div>
|
|
</div>
|
|
<div class="branding-mockup-main">
|
|
<div class="branding-mockup-card">
|
|
<h4 class="branding-mockup-heading">${_('branding.preview_card_title')}</h4>
|
|
<p class="branding-mockup-body">${_('branding.preview_card_body')}</p>
|
|
<div class="branding-mockup-actions">
|
|
<button type="button" class="branding-mockup-btn primary">${_('branding.preview_btn_primary')}</button>
|
|
<button type="button" class="branding-mockup-btn secondary">${_('branding.preview_btn_secondary')}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authentication Tab (server.config permission required) -->
|
|
<div class="settings-tab-content" id="tab-auth" role="tabpanel" aria-labelledby="settings-tab-auth" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<nav class="settings-auth-subnav" role="tablist" aria-label="${_('settings.tab_auth')}">
|
|
<button type="button" class="settings-auth-subtab active" role="tab" aria-selected="true" data-auth-panel="enrollment" id="settings-auth-tab-enrollment">
|
|
<span class="material-icons" aria-hidden="true">how_to_reg</span>
|
|
${_('settings.ui.auth_sub_enrollment')}
|
|
</button>
|
|
<button type="button" class="settings-auth-subtab" role="tab" aria-selected="false" data-auth-panel="client-sessions" id="settings-auth-tab-client-sessions">
|
|
<span class="material-icons" aria-hidden="true">devices</span>
|
|
${_('settings.ui.auth_sub_client_sessions')}
|
|
</button>
|
|
<button type="button" class="settings-auth-subtab" role="tab" aria-selected="false" data-auth-panel="ldap" id="settings-auth-tab-ldap">
|
|
<span class="material-icons" aria-hidden="true">domain</span>
|
|
${_('settings.ui.auth_sub_ldap')}
|
|
</button>
|
|
<button type="button" class="settings-auth-subtab" role="tab" aria-selected="false" data-auth-panel="oidc" id="settings-auth-tab-oidc">
|
|
<span class="material-icons" aria-hidden="true">login</span>
|
|
${_('settings.ui.auth_sub_oidc')}
|
|
</button>
|
|
</nav>
|
|
<div class="settings-auth-panel active" id="auth-panel-enrollment" role="tabpanel" aria-labelledby="settings-auth-tab-enrollment">
|
|
<div class="settings-stack">
|
|
<!-- Device enrollment -->
|
|
<div class="settings-panel" id="enrollment-settings-section">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">how_to_reg</span>
|
|
${_('settings.enrollment_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.enrollment_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="settings-choice-grid settings-enrollment-modes" role="radiogroup" aria-label="${_('tokens.enrollment_mode')}">
|
|
<label class="settings-choice-card enrollment-option">
|
|
<input type="radio" name="settings_enrollment_mode" value="open">
|
|
<span class="settings-choice-card-inner">
|
|
<span class="material-icons settings-choice-card-icon">lock_open</span>
|
|
<span class="settings-choice-card-title option-label">${_('tokens.mode_open')}</span>
|
|
<span class="settings-choice-card-desc option-desc">${_('tokens.mode_open_desc')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="settings-choice-card enrollment-option">
|
|
<input type="radio" name="settings_enrollment_mode" value="managed">
|
|
<span class="settings-choice-card-inner">
|
|
<span class="material-icons settings-choice-card-icon">supervisor_account</span>
|
|
<span class="settings-choice-card-title option-label">${_('tokens.mode_managed')}</span>
|
|
<span class="settings-choice-card-desc option-desc">${_('tokens.mode_managed_desc')}</span>
|
|
</span>
|
|
</label>
|
|
<label class="settings-choice-card enrollment-option">
|
|
<input type="radio" name="settings_enrollment_mode" value="locked">
|
|
<span class="settings-choice-card-inner">
|
|
<span class="material-icons settings-choice-card-icon">lock</span>
|
|
<span class="settings-choice-card-title option-label">${_('tokens.mode_locked')}</span>
|
|
<span class="settings-choice-card-desc option-desc">${_('tokens.mode_locked_desc')}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">
|
|
<label class="form-label" for="enrollment-require-approval">${_('settings.enrollment_require_approval')}</label>
|
|
<p class="settings-row-hint">${_('settings.enrollment_require_approval_hint')}</p>
|
|
</div>
|
|
<div class="settings-row-control">
|
|
<input type="checkbox" id="enrollment-require-approval" class="toggle-switch">
|
|
</div>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">
|
|
<label class="form-label" for="enrollment-rich-approve">${_('settings.enrollment_rich_approve')}</label>
|
|
<p class="settings-row-hint">${_('settings.enrollment_rich_approve_hint')}</p>
|
|
</div>
|
|
<div class="settings-row-control">
|
|
<input type="checkbox" id="enrollment-rich-approve" class="toggle-switch">
|
|
</div>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">
|
|
<label class="form-label" for="enrollment-tag-picker">${_('settings.enrollment_tag_picker')}</label>
|
|
<p class="settings-row-hint">${_('settings.enrollment_tag_picker_hint')}</p>
|
|
</div>
|
|
<div class="settings-row-control">
|
|
<input type="checkbox" id="enrollment-tag-picker" class="toggle-switch">
|
|
</div>
|
|
</div>
|
|
<p class="form-hint" id="enrollment-pending-hint"></p>
|
|
<p class="form-hint" style="margin-top: 8px;">${_('settings.enrollment_ldap_hint')}</p>
|
|
<div class="form-actions">
|
|
<a href="/registrations" class="btn btn-secondary btn-sm">
|
|
<span class="material-icons">pending_actions</span>
|
|
${_('settings.enrollment_open_requests')}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-auth-panel" id="auth-panel-client-sessions" role="tabpanel" aria-labelledby="settings-auth-tab-client-sessions" hidden>
|
|
<div class="settings-stack">
|
|
<div class="settings-panel" id="client-sessions-settings-section">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">devices</span>
|
|
${_('settings.client_sessions_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.client_sessions_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form" id="client-sessions-form" style="max-width: 520px;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="client-sessions-expiry-days">${_('settings.client_sessions_expiry_days')}</label>
|
|
<input type="number" id="client-sessions-expiry-days" class="form-input" min="1" max="365" value="7" style="max-width: 120px;">
|
|
<small class="form-hint">${_('settings.client_sessions_expiry_hint')}</small>
|
|
</div>
|
|
<div class="settings-row">
|
|
<div class="settings-row-label">
|
|
<label class="form-label" for="client-sessions-sliding">${_('settings.client_sessions_sliding')}</label>
|
|
<p class="settings-row-hint">${_('settings.client_sessions_sliding_hint')}</p>
|
|
</div>
|
|
<div class="settings-row-control">
|
|
<input type="checkbox" id="client-sessions-sliding" class="toggle-switch" checked>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="client-sessions-max-days">${_('settings.client_sessions_max_days')}</label>
|
|
<input type="number" id="client-sessions-max-days" class="form-input" min="1" max="365" value="30" style="max-width: 120px;">
|
|
<small class="form-hint">${_('settings.client_sessions_max_days_hint')}</small>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary btn-sm">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.client_sessions_save')}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-auth-panel" id="auth-panel-ldap" role="tabpanel" aria-labelledby="settings-auth-tab-ldap" hidden>
|
|
<div class="settings-stack">
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">domain</span>
|
|
${_('settings.ldap_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.ldap_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form" id="ldap-form" style="max-width: 700px;">
|
|
<!-- Enable toggle -->
|
|
<div class="form-group" style="display: flex; align-items: center; gap: 12px;">
|
|
<input type="checkbox" id="ldap-enabled" class="toggle-switch">
|
|
<div>
|
|
<label class="form-label" for="ldap-enabled" style="margin-bottom: 0; cursor: pointer;">${_('settings.ldap_enabled')}</label>
|
|
<small class="form-hint" style="display: block; margin-top: 2px;">${_('settings.ldap_enabled_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="ldap-config-fields">
|
|
<!-- Server connection -->
|
|
<h3 class="settings-subsection-title">${_('settings.ldap_connection')}</h3>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group" style="flex: 2;">
|
|
<label class="form-label" for="ldap-host">${_('settings.ldap_host')}</label>
|
|
<input type="text" id="ldap-host" class="form-input" placeholder="ldap.example.com">
|
|
</div>
|
|
<div class="form-group" style="flex: 1;">
|
|
<label class="form-label" for="ldap-port">${_('settings.ldap_port')}</label>
|
|
<input type="number" id="ldap-port" class="form-input" placeholder="389" min="1" max="65535">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" style="display: flex; gap: 24px; flex-wrap: wrap;">
|
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
<input type="checkbox" id="ldap-use-tls" class="toggle-switch">
|
|
<span>${_('settings.ldap_use_tls')}</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
<input type="checkbox" id="ldap-start-tls" class="toggle-switch">
|
|
<span>${_('settings.ldap_start_tls')}</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
<input type="checkbox" id="ldap-skip-tls-verify" class="toggle-switch">
|
|
<span>${_('settings.ldap_skip_verify')}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-conn-timeout">${_('settings.ldap_timeout')}</label>
|
|
<input type="number" id="ldap-conn-timeout" class="form-input" placeholder="10" min="1" max="120" style="max-width: 120px;">
|
|
<small class="form-hint">${_('settings.ldap_timeout_hint')}</small>
|
|
</div>
|
|
|
|
<!-- Bind mode -->
|
|
<h3 class="settings-subsection-title">${_('settings.ldap_bind_mode')}</h3>
|
|
|
|
<div class="form-group" style="display: flex; align-items: center; gap: 12px;">
|
|
<input type="checkbox" id="ldap-direct-bind" class="toggle-switch">
|
|
<div>
|
|
<label class="form-label" for="ldap-direct-bind" style="margin-bottom: 0; cursor: pointer;">${_('settings.ldap_direct_bind')}</label>
|
|
<small class="form-hint" style="display: block; margin-top: 2px;">${_('settings.ldap_direct_bind_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bind+Search fields (shown when direct_bind is off) -->
|
|
<div id="ldap-bind-search-fields">
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-bind-dn">${_('settings.ldap_bind_dn')}</label>
|
|
<input type="text" id="ldap-bind-dn" class="form-input" placeholder="cn=readonly,dc=example,dc=com">
|
|
<small class="form-hint">${_('settings.ldap_bind_dn_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-bind-password">${_('settings.ldap_bind_password')}</label>
|
|
<input type="password" id="ldap-bind-password" class="form-input" placeholder="••••••••">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-base-dn">${_('settings.ldap_base_dn')}</label>
|
|
<input type="text" id="ldap-base-dn" class="form-input" placeholder="dc=example,dc=com">
|
|
<small class="form-hint">${_('settings.ldap_base_dn_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-user-filter">${_('settings.ldap_user_filter')}</label>
|
|
<input type="text" id="ldap-user-filter" class="form-input" placeholder="(sAMAccountName={{username}})">
|
|
<small class="form-hint">${_('settings.ldap_user_filter_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Direct bind fields (shown when direct_bind is on) -->
|
|
<div id="ldap-direct-bind-fields" style="display: none;">
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-direct-bind-dn">${_('settings.ldap_direct_bind_dn')}</label>
|
|
<input type="text" id="ldap-direct-bind-dn" class="form-input" placeholder="uid={{username}},ou=users,dc=example,dc=com">
|
|
<small class="form-hint">${_('settings.ldap_direct_bind_dn_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User attributes -->
|
|
<h3 class="settings-subsection-title">${_('settings.ldap_user_attrs')}</h3>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-user-attr-id">${_('settings.ldap_attr_id')}</label>
|
|
<input type="text" id="ldap-user-attr-id" class="form-input" placeholder="sAMAccountName">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-user-attr-email">${_('settings.ldap_attr_email')}</label>
|
|
<input type="text" id="ldap-user-attr-email" class="form-input" placeholder="mail">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-user-attr-name">${_('settings.ldap_attr_name')}</label>
|
|
<input type="text" id="ldap-user-attr-name" class="form-input" placeholder="displayName">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Group mapping -->
|
|
<h3 class="settings-subsection-title">${_('settings.ldap_group_mapping')}</h3>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-group-base-dn">${_('settings.ldap_group_base_dn')}</label>
|
|
<input type="text" id="ldap-group-base-dn" class="form-input" placeholder="ou=groups,dc=example,dc=com">
|
|
<small class="form-hint">${_('settings.ldap_group_base_dn_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-group-filter">${_('settings.ldap_group_filter')}</label>
|
|
<input type="text" id="ldap-group-filter" class="form-input" placeholder="(member={{dn}})">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-group-attr-name">${_('settings.ldap_group_attr_name')}</label>
|
|
<input type="text" id="ldap-group-attr-name" class="form-input" placeholder="cn">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-default-role">${_('settings.ldap_default_role')}</label>
|
|
<select id="ldap-default-role" class="form-input" style="max-width: 300px;">
|
|
<option value="viewer">${_('settings.ldap_role_viewer')}</option>
|
|
<option value="operator">${_('settings.ldap_role_operator')}</option>
|
|
<option value="admin">${_('settings.ldap_role_admin')}</option>
|
|
</select>
|
|
<small class="form-hint">${_('settings.ldap_default_role_hint')}</small>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="ldap-group-role-map">${_('settings.ldap_group_role_map')}</label>
|
|
<textarea id="ldap-group-role-map" class="form-input form-textarea" rows="4"
|
|
placeholder="CN=BetterDesk Admins,OU=Groups,DC=example,DC=com=admin|CN=Operators,OU=Groups,DC=example,DC=com=operator"></textarea>
|
|
<small class="form-hint">${_('settings.ldap_group_role_map_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions" style="display: flex; gap: 12px; margin-top: 16px;">
|
|
<button type="button" class="btn btn-secondary" id="ldap-test-btn">
|
|
<span class="material-icons">cable</span>
|
|
${_('settings.ldap_test_connection')}
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.ldap_save')}
|
|
</button>
|
|
</div>
|
|
<div id="ldap-test-result" style="margin-top: 12px;"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="settings-auth-panel" id="auth-panel-oidc" role="tabpanel" aria-labelledby="settings-auth-tab-oidc" hidden>
|
|
<div class="settings-stack">
|
|
|
|
<!-- OIDC / OAuth2 / SSO -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">key</span>
|
|
${_('settings.oidc_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.oidc_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form class="settings-form" id="oidc-form" style="max-width: 700px;">
|
|
<!-- Enable toggle -->
|
|
<div class="form-group" style="display: flex; align-items: center; gap: 12px;">
|
|
<input type="checkbox" id="oidc-enabled" class="toggle-switch">
|
|
<div>
|
|
<label class="form-label" for="oidc-enabled" style="margin-bottom: 0; cursor: pointer;">${_('settings.oidc_enabled')}</label>
|
|
<small class="form-hint" style="display: block; margin-top: 2px;">${_('settings.oidc_enabled_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="oidc-config-fields">
|
|
<!-- Provider settings -->
|
|
<h3 class="settings-subsection-title">${_('settings.oidc_provider')}</h3>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-display-name">${_('settings.oidc_display_name')}</label>
|
|
<input type="text" id="oidc-display-name" class="form-input" placeholder="Azure AD / Okta / Google" style="max-width: 400px;">
|
|
<small class="form-hint">${_('settings.oidc_display_name_hint')}</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-issuer-url">${_('settings.oidc_issuer_url')}</label>
|
|
<input type="url" id="oidc-issuer-url" class="form-input" placeholder="https://login.microsoftonline.com/{tenant}/v2.0">
|
|
<small class="form-hint">${_('settings.oidc_issuer_url_hint')}</small>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group" style="flex: 2;">
|
|
<label class="form-label" for="oidc-client-id">${_('settings.oidc_client_id')}</label>
|
|
<input type="text" id="oidc-client-id" class="form-input" placeholder="your-client-id">
|
|
</div>
|
|
<div class="form-group" style="flex: 2;">
|
|
<label class="form-label" for="oidc-client-secret">${_('settings.oidc_client_secret')}</label>
|
|
<input type="password" id="oidc-client-secret" class="form-input" placeholder="••••••••">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-redirect-url">${_('settings.oidc_redirect_url')}</label>
|
|
<input type="url" id="oidc-redirect-url" class="form-input" placeholder="https://your-server:5443/api/auth/oidc/callback">
|
|
<small class="form-hint">${_('settings.oidc_redirect_url_hint')}</small>
|
|
<div id="oidc-redirect-url-warning" class="alert alert-warning" style="display:none;margin-top:8px;" role="status"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-panel-url">${_('settings.oidc_panel_url')}</label>
|
|
<input type="url" id="oidc-panel-url" class="form-input" placeholder="http://your-server:5000">
|
|
<small class="form-hint">${_('settings.oidc_panel_url_hint')}</small>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-scopes">${_('settings.oidc_scopes')}</label>
|
|
<input type="text" id="oidc-scopes" class="form-input" placeholder="openid profile email" style="max-width: 500px;">
|
|
</div>
|
|
|
|
<div class="form-group" style="display: flex; gap: 24px; flex-wrap: wrap;">
|
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
<input type="checkbox" id="oidc-use-pkce" class="toggle-switch">
|
|
<span>${_('settings.oidc_use_pkce')}</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
<input type="checkbox" id="oidc-auto-discovery" class="toggle-switch" checked>
|
|
<span>${_('settings.oidc_auto_discovery')}</span>
|
|
</label>
|
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
|
<input type="checkbox" id="oidc-allow-signup" class="toggle-switch" checked>
|
|
<span>${_('settings.oidc_allow_signup')}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Manual endpoints (shown when auto_discovery is off) -->
|
|
<div id="oidc-manual-endpoints" style="display: none;">
|
|
<h3 class="settings-subsection-title">${_('settings.oidc_manual_endpoints')}</h3>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-authorization-url">${_('settings.oidc_authorization_url')}</label>
|
|
<input type="url" id="oidc-authorization-url" class="form-input" placeholder="https://idp.example.com/authorize">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-token-url">${_('settings.oidc_token_url')}</label>
|
|
<input type="url" id="oidc-token-url" class="form-input" placeholder="https://idp.example.com/token">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-userinfo-url">${_('settings.oidc_userinfo_url')}</label>
|
|
<input type="url" id="oidc-userinfo-url" class="form-input" placeholder="https://idp.example.com/userinfo">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Claim mapping -->
|
|
<h3 class="settings-subsection-title">${_('settings.oidc_claim_mapping')}</h3>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-claim-username">${_('settings.oidc_claim_username')}</label>
|
|
<input type="text" id="oidc-claim-username" class="form-input" placeholder="preferred_username">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-claim-email">${_('settings.oidc_claim_email')}</label>
|
|
<input type="text" id="oidc-claim-email" class="form-input" placeholder="email">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-claim-name">${_('settings.oidc_claim_name')}</label>
|
|
<input type="text" id="oidc-claim-name" class="form-input" placeholder="name">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-claim-groups">${_('settings.oidc_claim_groups')}</label>
|
|
<input type="text" id="oidc-claim-groups" class="form-input" placeholder="groups" style="max-width: 300px;">
|
|
</div>
|
|
|
|
<!-- Role mapping -->
|
|
<h3 class="settings-subsection-title">${_('settings.oidc_role_mapping')}</h3>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-default-role">${_('settings.oidc_default_role')}</label>
|
|
<select id="oidc-default-role" class="form-input" style="max-width: 300px;">
|
|
<option value="viewer">${_('settings.ldap_role_viewer')}</option>
|
|
<option value="operator">${_('settings.ldap_role_operator')}</option>
|
|
<option value="admin">${_('settings.ldap_role_admin')}</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="oidc-group-role-map">${_('settings.oidc_group_role_map')}</label>
|
|
<textarea id="oidc-group-role-map" class="form-input form-textarea" rows="3"
|
|
placeholder="betterdesk-admins=admin betterdesk-operators=operator"></textarea>
|
|
<small class="form-hint">${_('settings.oidc_group_role_map_hint')}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-actions" style="display: flex; gap: 12px; margin-top: 16px;">
|
|
<button type="button" class="btn btn-secondary" id="oidc-test-btn">
|
|
<span class="material-icons">travel_explore</span>
|
|
${_('settings.oidc_test_discovery')}
|
|
</button>
|
|
<button type="submit" class="btn btn-primary">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.oidc_save')}
|
|
</button>
|
|
</div>
|
|
<div id="oidc-test-result" style="margin-top: 12px;"></div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup & Restore Tab (server.config permission required) -->
|
|
<div class="settings-tab-content" id="tab-backup" role="tabpanel" aria-labelledby="settings-tab-backup" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<div class="settings-stack">
|
|
<!-- Backup Info -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">backup</span>
|
|
${_('backup.title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('backup.desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="backup-stats-grid" id="backup-stats">
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_users')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-users">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_settings')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-settings">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_folders')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-folders">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_groups')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-groups">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_strategies')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-strategies">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_backend')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-backend">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_database')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-database">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_keys')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-keys">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_env')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-env">-</span>
|
|
</div>
|
|
<div class="backup-stat-card">
|
|
<span class="backup-stat-label">${_('backup.stat_uploads')}</span>
|
|
<span class="backup-stat-value" id="backup-stat-uploads">-</span>
|
|
</div>
|
|
</div>
|
|
<div class="restore-warning-box">
|
|
<span class="material-icons">warning</span>
|
|
<span>${_('backup.security_warning')}</span>
|
|
</div>
|
|
<div class="backup-actions">
|
|
<button class="btn btn-primary" id="backup-download-btn">
|
|
<span class="material-icons">download</span>
|
|
${_('backup.download')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Restore -->
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">restore</span>
|
|
${_('backup.restore_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('backup.restore_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="restore-options">
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-database" checked>
|
|
<span>${_('backup.opt_database')}</span>
|
|
<small class="restore-hint">${_('backup.opt_database_hint')}</small>
|
|
</label>
|
|
<label class="restore-option">
|
|
<input type="checkbox" id="restore-uploads" checked>
|
|
<span>${_('backup.opt_uploads')}</span>
|
|
<small class="restore-hint">${_('backup.opt_uploads_hint')}</small>
|
|
</label>
|
|
<label class="restore-option restore-option-danger">
|
|
<input type="checkbox" id="restore-secrets">
|
|
<span>${_('backup.opt_secrets')}</span>
|
|
<small class="restore-warning">${_('backup.opt_secrets_warning')}</small>
|
|
</label>
|
|
<label class="restore-option restore-option-danger">
|
|
<input type="checkbox" id="restore-env">
|
|
<span>${_('backup.opt_env')}</span>
|
|
<small class="restore-warning">${_('backup.opt_env_warning')}</small>
|
|
</label>
|
|
<label class="restore-option restore-option-danger">
|
|
<input type="checkbox" id="restore-godb">
|
|
<span>${_('backup.opt_godb')}</span>
|
|
<small class="restore-warning">${_('backup.opt_godb_warning')}</small>
|
|
</label>
|
|
</div>
|
|
<div class="restore-warning-box restore-warning-box-danger">
|
|
<span class="material-icons">priority_high</span>
|
|
<span>${_('backup.restore_warning')}</span>
|
|
</div>
|
|
<div class="backup-actions">
|
|
<label class="btn btn-warning" id="restore-upload-label">
|
|
<span class="material-icons">upload</span>
|
|
${_('backup.upload')}
|
|
<input type="file" id="restore-file-input" accept=".gz,.tar.gz,.json" hidden>
|
|
</label>
|
|
</div>
|
|
<div class="restore-result" id="restore-result" style="display:none;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Updates Tab (server.config permission required) -->
|
|
<div class="settings-tab-content" id="tab-updates" role="tabpanel" aria-labelledby="settings-tab-updates" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<div class="win11-updates">
|
|
<h1 class="win11-updates-title">${_('updates.title')}</h1>
|
|
|
|
<!-- Hero status (Windows 11 Update style) -->
|
|
<section class="win11-updates-hero" aria-labelledby="update-status-headline">
|
|
<div class="win11-updates-hero-icon" id="update-hero-icon" data-state="idle" aria-hidden="true">
|
|
<svg class="win11-updates-hero-svg" viewBox="0 0 48 48" width="72" height="72" focusable="false">
|
|
<circle cx="24" cy="24" r="20" fill="none" stroke="currentColor" stroke-width="2.5" opacity="0.25"/>
|
|
<path fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
|
d="M24 8a16 16 0 0 1 14.5 9.2M24 40a16 16 0 0 1-14.5-9.2M8 24h6M34 24h6"/>
|
|
</svg>
|
|
<span class="win11-updates-hero-badge" id="update-hero-badge" hidden>
|
|
<span class="material-icons">priority_high</span>
|
|
</span>
|
|
</div>
|
|
<div class="win11-updates-hero-body">
|
|
<h2 class="win11-updates-hero-headline" id="update-status-headline">${_('updates.title')}</h2>
|
|
<p class="win11-updates-hero-desc" id="update-status-desc">${_('updates.desc')}</p>
|
|
<dl class="update-version-info win11-updates-meta">
|
|
<div class="update-version-row">
|
|
<dt class="update-label">${_('updates.current_version')}</dt>
|
|
<dd class="update-value" id="update-local-version">v${appVersion}</dd>
|
|
</div>
|
|
<div class="update-version-row">
|
|
<dt class="update-label">${_('updates.latest_version')}</dt>
|
|
<dd class="update-value" id="update-remote-version"><span class="text-muted">—</span></dd>
|
|
</div>
|
|
<div class="update-version-row" id="update-status-row" style="display:none;">
|
|
<dt class="update-label">${_('updates.status')}</dt>
|
|
<dd class="update-value" id="update-status-badge"></dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
<div class="win11-updates-hero-action">
|
|
<button class="btn win11-updates-primary-btn" id="update-check-btn" type="button">
|
|
<span class="material-icons">refresh</span>
|
|
${_('updates.check_now')}
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="win11-updates-alerts" id="update-alerts">
|
|
<!-- Docker image deployment -->
|
|
<div class="update-docker-info win11-updates-alert" id="update-docker-info" style="display:none;">
|
|
<div class="update-docker-icon">
|
|
<span class="material-icons">cloud_download</span>
|
|
</div>
|
|
<div class="update-docker-body">
|
|
<strong>${_('updates.docker_title')}</strong>
|
|
<p id="update-docker-desc">${_('updates.docker_desc')}</p>
|
|
<pre class="update-docker-commands" id="update-docker-commands"></pre>
|
|
<p class="update-docker-images" id="update-docker-images"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stale server binary warning -->
|
|
<div class="update-stale-warning win11-updates-alert" id="update-stale-warning" style="display:none;">
|
|
<div class="update-stale-icon">
|
|
<span class="material-icons">gpp_maybe</span>
|
|
</div>
|
|
<div class="update-stale-body">
|
|
<strong>${_('updates.server_stale_title')}</strong>
|
|
<p id="update-stale-detail">${_('updates.server_stale_desc')}</p>
|
|
<button class="btn btn-warning" id="update-rebuild-server-btn" type="button">
|
|
<span class="material-icons">build</span>
|
|
${_('updates.rebuild_server')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Available update package (shown after check) -->
|
|
<section class="win11-updates-available" id="update-details-section" style="display:none;" aria-labelledby="update-available-heading">
|
|
<h2 class="win11-updates-available-title" id="update-available-heading">${_('updates.update_available')}</h2>
|
|
<div class="win11-updates-available-body">
|
|
<div class="update-changelog" id="update-changelog"></div>
|
|
<div class="update-files-summary" id="update-files-summary"></div>
|
|
<div class="win11-updates-install-row">
|
|
<label class="win11-updates-checkbox restore-option">
|
|
<input type="checkbox" id="update-backup-toggle" checked>
|
|
<span>${_('updates.create_backup')}</span>
|
|
</label>
|
|
<button class="btn win11-updates-primary-btn win11-updates-install-btn" id="update-install-btn" type="button" disabled>
|
|
<span class="material-icons">download</span>
|
|
${_('updates.install')}
|
|
</button>
|
|
<p class="update-docker-install-note text-muted" id="update-docker-install-note" style="display:none;">
|
|
${_('updates.docker_install_note')}
|
|
</p>
|
|
</div>
|
|
<div class="update-progress" id="update-progress" style="display:none;">
|
|
<div class="update-progress-bar">
|
|
<div class="update-progress-fill" id="update-progress-fill"></div>
|
|
</div>
|
|
<p class="update-progress-text" id="update-progress-text">${_('updates.installing')}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<h2 class="win11-updates-section-title">${_('updates.more_options')}</h2>
|
|
<div class="win11-updates-options">
|
|
|
|
<!-- Update channel (inline control like Win11 pause dropdown) -->
|
|
<div class="win11-updates-option win11-updates-option-inline" id="update-channel-panel">
|
|
<div class="win11-updates-option-row">
|
|
<span class="win11-updates-option-icon material-icons" aria-hidden="true">alt_route</span>
|
|
<div class="win11-updates-option-text">
|
|
<span class="win11-updates-option-title">${_('updates.channel_title')}</span>
|
|
<span class="win11-updates-option-desc">${_('updates.channel_desc')}</span>
|
|
</div>
|
|
<select id="update-channel-select" class="win11-updates-select" aria-label="${_('updates.channel_pick_label')}">
|
|
<option value="stable">${_('updates.channel_stable')}</option>
|
|
<option value="development">${_('updates.channel_development')}</option>
|
|
</select>
|
|
</div>
|
|
<div class="win11-updates-option-meta">
|
|
<span id="update-channel-active-badge" class="badge badge-success badge-sm">—</span>
|
|
<span class="win11-updates-option-meta-sep" aria-hidden="true">·</span>
|
|
<span class="text-muted">${_('updates.channel_branch_label')}:</span>
|
|
<span id="update-channel-branch">—</span>
|
|
<p class="win11-updates-option-hint">${_('updates.channel_apply_hint')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Update history / backups -->
|
|
<details class="win11-updates-option">
|
|
<summary class="win11-updates-option-row">
|
|
<span class="win11-updates-option-icon material-icons" aria-hidden="true">history</span>
|
|
<div class="win11-updates-option-text">
|
|
<span class="win11-updates-option-title">${_('updates.backups_title')}</span>
|
|
<span class="win11-updates-option-desc">${_('updates.backups_desc')}</span>
|
|
</div>
|
|
<span class="win11-updates-option-chevron material-icons" aria-hidden="true">chevron_right</span>
|
|
</summary>
|
|
<div class="win11-updates-option-body">
|
|
<div class="backup-retention-row">
|
|
<label for="backup-retention-input" class="backup-retention-label">
|
|
${_('updates.retention_label')}
|
|
<small class="text-muted">${_('updates.retention_hint')}</small>
|
|
</label>
|
|
<div class="backup-retention-controls">
|
|
<input type="number" id="backup-retention-input" min="0" max="1000" step="1" value="0">
|
|
<button class="btn btn-sm btn-primary" id="backup-retention-save" type="button">
|
|
${_('common.save')}
|
|
</button>
|
|
<button class="btn btn-sm btn-outline" id="backup-prune-now" type="button">
|
|
<span class="material-icons">cleaning_services</span>
|
|
${_('updates.prune_now')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="backup-summary text-muted" id="backup-summary"></div>
|
|
<div id="update-backups-list">
|
|
<p class="text-muted">${_('common.loading')}</p>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Advanced options -->
|
|
<details class="win11-updates-option">
|
|
<summary class="win11-updates-option-row">
|
|
<span class="win11-updates-option-icon material-icons" aria-hidden="true">tune</span>
|
|
<div class="win11-updates-option-text">
|
|
<span class="win11-updates-option-title">${_('updates.option_advanced')}</span>
|
|
<span class="win11-updates-option-desc">${_('updates.option_advanced_desc')}</span>
|
|
</div>
|
|
<span class="win11-updates-option-chevron material-icons" aria-hidden="true">chevron_right</span>
|
|
</summary>
|
|
<div class="win11-updates-option-body win11-updates-advanced-body">
|
|
<p class="text-muted win11-updates-advanced-note">${_('updates.auto_strategy_hint')}</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Email / SMTP Tab -->
|
|
<div class="settings-tab-content" id="tab-email" role="tabpanel" aria-labelledby="settings-tab-email" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<div class="settings-stack">
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">email</span>
|
|
${_('settings.email.title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.email.desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<form id="email-smtp-form" class="settings-form">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="email-smtp-host">${_('settings.email.smtp_host')}</label>
|
|
<input type="text" id="email-smtp-host" class="form-input" placeholder="smtp.example.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="email-smtp-port">${_('settings.email.smtp_port')}</label>
|
|
<input type="number" id="email-smtp-port" class="form-input" value="587" min="1" max="65535">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">
|
|
<input type="checkbox" id="email-smtp-secure"> ${_('settings.email.smtp_secure')}
|
|
</label>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label" for="email-smtp-user">${_('settings.email.smtp_user')}</label>
|
|
<input type="text" id="email-smtp-user" class="form-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="email-smtp-pass">${_('settings.email.smtp_pass')}</label>
|
|
<input type="password" id="email-smtp-pass" class="form-input" autocomplete="new-password">
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="email-smtp-from">${_('settings.email.smtp_from')}</label>
|
|
<input type="email" id="email-smtp-from" class="form-input" placeholder="alerts@example.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label" for="email-smtp-alert">${_('settings.email.alert_email')}</label>
|
|
<input type="email" id="email-smtp-alert" class="form-input" placeholder="ops@example.com">
|
|
<span class="form-hint">${_('settings.email.alert_email_hint')}</span>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" id="email-smtp-test-btn">${_('settings.email.smtp_test')}</button>
|
|
<button type="button" class="btn btn-primary" id="email-smtp-save-btn">${_('settings.email.smtp_save')}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced config files (server.config permission required) -->
|
|
<div class="settings-tab-content" id="tab-advanced" role="tabpanel" aria-labelledby="settings-tab-advanced" ${canServerConfig ? '' : 'style="display:none"'}>
|
|
<div class="settings-stack">
|
|
<div class="settings-panel">
|
|
<div class="settings-panel-header">
|
|
<h2 class="settings-panel-title">
|
|
<span class="material-icons">code</span>
|
|
${_('settings.advanced_title')}
|
|
</h2>
|
|
<p class="settings-panel-desc">${_('settings.advanced_desc')}</p>
|
|
</div>
|
|
<div class="settings-panel-body">
|
|
<div class="restore-warning-box">
|
|
<span class="material-icons">warning</span>
|
|
<span>${_('settings.advanced_warning')}</span>
|
|
</div>
|
|
<div class="advanced-config-layout">
|
|
<aside class="advanced-config-sidebar">
|
|
<p class="advanced-config-sidebar-label">${_('settings.advanced_files_label')}</p>
|
|
<ul class="advanced-config-file-list" id="advanced-config-file-list">
|
|
<li class="text-muted">${_('common.loading')}</li>
|
|
</ul>
|
|
</aside>
|
|
<div class="advanced-config-editor">
|
|
<div class="advanced-config-editor-toolbar" id="advanced-config-toolbar" hidden>
|
|
<div class="advanced-config-editor-meta">
|
|
<code class="advanced-config-path" id="advanced-config-path"></code>
|
|
<span class="text-muted" id="advanced-config-meta"></span>
|
|
</div>
|
|
<div class="advanced-config-editor-actions">
|
|
<button type="button" class="btn btn-sm btn-outline" id="advanced-config-reload">
|
|
<span class="material-icons">refresh</span>
|
|
${_('settings.advanced_reload')}
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="advanced-config-save">
|
|
<span class="material-icons">save</span>
|
|
${_('settings.advanced_save')}
|
|
</button>
|
|
<button type="button" class="btn btn-sm btn-warning" id="advanced-config-restart" hidden>
|
|
<span class="material-icons">restart_alt</span>
|
|
${_('settings.advanced_apply_restart')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<p class="advanced-config-placeholder text-muted" id="advanced-config-placeholder">${_('settings.advanced_select_file')}</p>
|
|
<textarea class="advanced-config-textarea" id="advanced-config-textarea" spellcheck="false" hidden></textarea>
|
|
<p class="advanced-config-restart-hint text-muted" id="advanced-config-restart-hint" hidden></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- .settings-shell-main -->
|
|
</div><!-- .settings-shell -->
|
|
`
|
|
}) %>
|