mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-10 09:35:39 +00:00
6a1806b8f3
Bring stable signal outbound gate hardening (initiator_pending_enrollment), Web Remote SignedId verify, and open-issue hotfixes onto dev while keeping the 3.4.13 development version baseline.
2384 lines
152 KiB
HTML
2384 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>
|
|
<p id="update-channel-docker-hint" class="win11-updates-option-hint" style="display:none"></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 -->
|
|
`
|
|
}) %>
|