Files
BetterDesk/web-nodejs/views/settings.ejs
T
UNITRONIX f8139432a2 fix(console): update channel UX and Modal.confirm (main hotfix)
Apply channel on select with confirmation, active channel badge,
Modal.confirm instead of nonexistent Modal.open.
2026-06-07 17:07:20 +02:00

1696 lines
104 KiB
HTML

<%- include('layouts/main', {
title: _('nav.settings'),
pageStyles: ['pages'],
pageScripts: ['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>
<!-- Settings Tab Navigation -->
<div class="settings-tabs">
<button class="settings-tab active" data-tab="general">
<span class="material-icons">tune</span>
${_('settings.tab_general')}
</button>
<button class="settings-tab" data-tab="branding">
<span class="material-icons">palette</span>
${_('settings.tab_branding')}
</button>
${canServerConfig ? `<button class="settings-tab" data-tab="auth">
<span class="material-icons">admin_panel_settings</span>
${_('settings.tab_auth')}
</button>
<button class="settings-tab" data-tab="backup">
<span class="material-icons">backup</span>
${_('settings.tab_backup')}
</button>
<button class="settings-tab" data-tab="updates">
<span class="material-icons">system_update</span>
${_('updates.tab_title')}
</button>
<button class="settings-tab" data-tab="advanced">
<span class="material-icons">code</span>
${_('settings.tab_advanced')}
</button>` : ''}
</div>
<!-- General Tab -->
<div class="settings-tab-content active" id="tab-general">
<div class="settings-sections">
<!-- Password Change Section -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">lock</span>
${_('settings.change_password')}
</h2>
<p class="settings-section-desc">${_('settings.password_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">info</span>
${_('settings.server_info')}
</h2>
</div>
<div class="settings-section-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">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 ? `
<!-- Connection Strategy (P2P / Relay) -->
<div class="settings-section" id="connection-mode-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">swap_horiz</span>
${_('settings.connection_mode_title')}
</h2>
<p class="settings-section-desc">${_('settings.connection_mode_desc')}</p>
</div>
<div class="settings-section-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="form-group">
<label class="form-label">${_('settings.connection_mode_label')}</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="connection-mode" value="p2p_first" id="conn-mode-p2p">
<span>${_('settings.connection_mode_p2p_first')}</span>
</label>
<label class="radio-label">
<input type="radio" name="connection-mode" value="relay_only" id="conn-mode-relay">
<span>${_('settings.connection_mode_relay_only')}</span>
</label>
</div>
</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>
` : ''}
<!-- Two-Factor Authentication Section -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">security</span>
${_('settings.totp_title')}
</h2>
<p class="settings-section-desc">${_('settings.totp_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">history</span>
${_('settings.audit_log')}
</h2>
<p class="settings-section-desc">${_('settings.audit_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">school</span>
${_('settings.tutorials_title')}
</h2>
<p class="settings-section-desc">${_('settings.tutorials_desc')}</p>
</div>
<div class="settings-section-body">
<div class="form-group" style="max-width: 600px; display: flex; align-items: center; gap: 12px;">
<input type="checkbox" id="tutorials-enabled" class="toggle-switch">
<div>
<label class="form-label" for="tutorials-enabled" style="margin-bottom: 0; cursor: pointer;">${_('settings.tutorials_enabled')}</label>
<small class="form-hint" style="display: block; margin-top: 2px;">${_('settings.tutorials_enabled_hint')}</small>
</div>
</div>
<div class="form-group" style="max-width: 600px;">
<button class="btn btn-secondary btn-sm" id="tutorials-reset-btn">
<span class="material-icons">refresh</span>
${_('tutorial.reset_all')}
</button>
<small class="form-hint" style="display: block; margin-top: 6px;">${_('settings.tutorials_reset_hint')}</small>
</div>
</div>
</div>
</div>
</div>
<!-- Branding Tab -->
<div class="settings-tab-content" id="tab-branding">
<div class="settings-sections">
<!-- Brand Identity -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">badge</span>
${_('branding.identity_title')}
</h2>
<p class="settings-section-desc">${_('branding.identity_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">image</span>
${_('branding.logo_title')}
</h2>
<p class="settings-section-desc">${_('branding.logo_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">palette</span>
${_('branding.colors_title')}
</h2>
<p class="settings-section-desc">${_('branding.colors_desc')}</p>
</div>
<div class="settings-section-body">
<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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">text_format</span>
${_('branding.typography_title')}
</h2>
<p class="settings-section-desc">${_('branding.typography_desc')}</p>
</div>
<div class="settings-section-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>
<!-- 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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">wallpaper</span>
${_('branding.background_title')}
</h2>
<p class="settings-section-desc">${_('branding.background_desc')}</p>
</div>
<div class="settings-section-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="form-group">
<label class="form-label">${_('branding.bg_image_upload')}</label>
<div class="logo-upload-area">
<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>
<span id="bg-file-name" class="logo-file-name"></span>
</div>
</div>
<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>
<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>
</div>
</div>
<!-- Login Page -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">login</span>
${_('branding.login_title')}
</h2>
<p class="settings-section-desc">${_('branding.login_desc')}</p>
</div>
<div class="settings-section-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>
<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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">download</span>
${_('branding.agent_title')}
</h2>
<p class="settings-section-desc">${_('branding.agent_desc')}</p>
</div>
<div class="settings-section-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>
<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>
<!-- Footer & Copyright -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">copyright</span>
${_('branding.footer_title')}
</h2>
<p class="settings-section-desc">${_('branding.footer_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">code</span>
${_('branding.custom_css_title')}
</h2>
<p class="settings-section-desc">${_('branding.custom_css_desc')}</p>
</div>
<div class="settings-section-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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">swap_horiz</span>
${_('branding.import_export_title')}
</h2>
<p class="settings-section-desc">${_('branding.import_export_desc')}</p>
</div>
<div class="settings-section-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>
<!-- Save Branding Button -->
<div class="branding-save-bar">
<button class="btn btn-primary btn-lg" id="branding-save-btn">
<span class="material-icons">save</span>
${_('branding.save_branding')}
</button>
</div>
</div>
</div>
<!-- Authentication Tab (server.config permission required) -->
<div class="settings-tab-content" id="tab-auth" ${canServerConfig ? '' : 'style="display:none"'}>
<div class="settings-sections">
<!-- Device enrollment -->
<div class="settings-section" id="enrollment-settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">how_to_reg</span>
${_('settings.enrollment_title')}
</h2>
<p class="settings-section-desc">${_('settings.enrollment_desc')}</p>
</div>
<div class="settings-section-body" style="max-width: 720px;">
<div class="enrollment-mode-options settings-enrollment-modes">
<label class="enrollment-option">
<input type="radio" name="settings_enrollment_mode" value="open">
<span class="option-label">${_('tokens.mode_open')}</span>
<span class="option-desc">${_('tokens.mode_open_desc')}</span>
</label>
<label class="enrollment-option">
<input type="radio" name="settings_enrollment_mode" value="managed">
<span class="option-label">${_('tokens.mode_managed')}</span>
<span class="option-desc">${_('tokens.mode_managed_desc')}</span>
</label>
<label class="enrollment-option">
<input type="radio" name="settings_enrollment_mode" value="locked">
<span class="option-label">${_('tokens.mode_locked')}</span>
<span class="option-desc">${_('tokens.mode_locked_desc')}</span>
</label>
</div>
<div class="form-group form-check" style="margin-top: 16px;">
<label class="check-label">
<input type="checkbox" id="enrollment-require-approval" class="toggle-switch">
<span>${_('settings.enrollment_require_approval')}</span>
</label>
<p class="form-hint">${_('settings.enrollment_require_approval_hint')}</p>
</div>
<div class="form-group form-check">
<label class="check-label">
<input type="checkbox" id="enrollment-rich-approve" class="toggle-switch">
<span>${_('settings.enrollment_rich_approve')}</span>
</label>
<p class="form-hint">${_('settings.enrollment_rich_approve_hint')}</p>
</div>
<div class="form-group form-check">
<label class="check-label">
<input type="checkbox" id="enrollment-tag-picker" class="toggle-switch">
<span>${_('settings.enrollment_tag_picker')}</span>
</label>
<p class="form-hint">${_('settings.enrollment_tag_picker_hint')}</p>
</div>
<p class="form-hint" id="enrollment-pending-hint" style="margin-top: 8px;"></p>
<div style="margin-top: 12px; display: flex; gap: 12px; flex-wrap: wrap;">
<a href="/registrations" class="btn btn-secondary btn-sm">
<span class="material-icons">pending_actions</span>
${_('settings.enrollment_open_requests')}
</a>
</div>
</div>
</div>
<!-- LDAP / Active Directory -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">domain</span>
${_('settings.ldap_title')}
</h2>
<p class="settings-section-desc">${_('settings.ldap_desc')}</p>
</div>
<div class="settings-section-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>
<!-- OIDC / OAuth2 / SSO -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">key</span>
${_('settings.oidc_title')}
</h2>
<p class="settings-section-desc">${_('settings.oidc_desc')}</p>
</div>
<div class="settings-section-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="http://your-server:21114/api/auth/oidc/callback">
<small class="form-hint">${_('settings.oidc_redirect_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&#10;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>
<!-- Backup & Restore Tab (server.config permission required) -->
<div class="settings-tab-content" id="tab-backup" ${canServerConfig ? '' : 'style="display:none"'}>
<div class="settings-sections">
<!-- Backup Info -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">backup</span>
${_('backup.title')}
</h2>
<p class="settings-section-desc">${_('backup.desc')}</p>
</div>
<div class="settings-section-body">
<div class="backup-stats" id="backup-stats">
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_users')}</span>
<span class="backup-stat-value" id="backup-stat-users">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_settings')}</span>
<span class="backup-stat-value" id="backup-stat-settings">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_folders')}</span>
<span class="backup-stat-value" id="backup-stat-folders">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_groups')}</span>
<span class="backup-stat-value" id="backup-stat-groups">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_strategies')}</span>
<span class="backup-stat-value" id="backup-stat-strategies">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_backend')}</span>
<span class="backup-stat-value" id="backup-stat-backend">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_database')}</span>
<span class="backup-stat-value" id="backup-stat-database">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_keys')}</span>
<span class="backup-stat-value" id="backup-stat-keys">-</span>
</div>
<div class="backup-stat-row">
<span class="backup-stat-label">${_('backup.stat_env')}</span>
<span class="backup-stat-value" id="backup-stat-env">-</span>
</div>
<div class="backup-stat-row">
<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-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">restore</span>
${_('backup.restore_title')}
</h2>
<p class="settings-section-desc">${_('backup.restore_desc')}</p>
</div>
<div class="settings-section-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" ${canServerConfig ? '' : 'style="display:none"'}>
<div class="settings-sections">
<!-- Version Check -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">system_update</span>
${_('updates.title')}
</h2>
<p class="settings-section-desc">${_('updates.desc')}</p>
</div>
<div class="settings-section-body">
<div class="update-channel-panel" id="update-channel-panel" style="margin-bottom: 20px; padding: 16px; border: 1px solid var(--border-color, #333); border-radius: 8px;">
<div class="update-version-row" style="margin-bottom: 8px;">
<span class="update-label">${_('updates.channel_title')}</span>
<span class="update-value">
<span id="update-channel-active-badge" class="badge badge-success badge-sm">&mdash;</span>
</span>
</div>
<p class="settings-section-desc" style="margin-top: 0; margin-bottom: 12px;">${_('updates.channel_desc')}</p>
<div class="update-version-row">
<span class="update-label">${_('updates.channel_pick_label')}</span>
<span class="update-value">
<select id="update-channel-select" class="form-input" style="max-width: 280px;">
<option value="stable">${_('updates.channel_stable')}</option>
<option value="development">${_('updates.channel_development')}</option>
</select>
</span>
</div>
<p class="text-muted" style="font-size: 12px; margin-top: 8px; margin-bottom: 0;">${_('updates.channel_apply_hint')}</p>
<div class="update-version-row" style="margin-top: 10px;">
<span class="update-label">${_('updates.channel_branch_label')}</span>
<span class="update-value" id="update-channel-branch">&mdash;</span>
</div>
</div>
<div class="update-version-info">
<div class="update-version-row">
<span class="update-label">${_('updates.current_version')}</span>
<span class="update-value" id="update-local-version">v${appVersion}</span>
</div>
<div class="update-version-row">
<span class="update-label">${_('updates.latest_version')}</span>
<span class="update-value" id="update-remote-version">
<span class="text-muted">&mdash;</span>
</span>
</div>
<div class="update-version-row" id="update-status-row" style="display:none;">
<span class="update-label">${_('updates.status')}</span>
<span class="update-value" id="update-status-badge"></span>
</div>
</div>
<div class="update-actions" style="margin-top: 16px;">
<button class="btn btn-primary" id="update-check-btn">
<span class="material-icons">refresh</span>
${_('updates.check_now')}
</button>
</div>
<!-- Docker image deployment: updates via container pull -->
<div class="update-docker-info" id="update-docker-info" style="display:none; margin-top:16px;">
<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 (security): server source was
updated but the running binary was not rebuilt. -->
<div class="update-stale-warning" id="update-stale-warning" style="display:none; margin-top:16px;">
<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">
<span class="material-icons">build</span>
${_('updates.rebuild_server')}
</button>
</div>
</div>
</div>
</div>
<!-- Update Details (hidden until check completes) -->
<div class="settings-section" id="update-details-section" style="display:none;">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">description</span>
${_('updates.changelog')}
</h2>
</div>
<div class="settings-section-body">
<div class="update-changelog" id="update-changelog"></div>
<div class="update-files-summary" id="update-files-summary" style="margin-top:12px;"></div>
<div class="update-actions" style="margin-top: 16px;">
<label class="restore-option" style="margin-bottom: 12px;">
<input type="checkbox" id="update-backup-toggle" checked>
<span>${_('updates.create_backup')}</span>
</label>
<button class="btn btn-primary btn-lg" id="update-install-btn" 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; margin-top:12px;">
${_('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>
</div>
<!-- Update Backups -->
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">history</span>
${_('updates.backups_title')}
</h2>
<p class="settings-section-desc">${_('updates.backups_desc')}</p>
</div>
<div class="settings-section-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">
${_('common.save')}
</button>
<button class="btn btn-sm btn-outline" id="backup-prune-now">
<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>
</div>
</div>
</div>
<!-- Advanced config files (server.config permission required) -->
<div class="settings-tab-content" id="tab-advanced" ${canServerConfig ? '' : 'style="display:none"'}>
<div class="settings-sections">
<div class="settings-section">
<div class="settings-section-header">
<h2 class="settings-section-title">
<span class="material-icons">code</span>
${_('settings.advanced_title')}
</h2>
<p class="settings-section-desc">${_('settings.advanced_desc')}</p>
</div>
<div class="settings-section-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>
`
}) %>