Files
pvemanager/backend/app/templates/settings.html

2129 lines
76 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<div class="settings-container">
<!-- Tabs Navigation -->
<div class="tabs-nav">
<button class="tab-btn active" data-tab="account">
<i class="fas fa-user"></i>
<span>{{ t('settings_account') }}</span>
</button>
<button class="tab-btn" data-tab="notifications">
<i class="fas fa-bell"></i>
<span>{{ t('notifications') }}</span>
</button>
<button class="tab-btn" data-tab="panel">
<i class="fas fa-cog"></i>
<span>{{ t('settings_panel') }}</span>
</button>
</div>
<!-- Account Settings Tab -->
<div id="account-tab" class="tab-content active">
<div class="settings-section">
<h2><i class="fa-solid fa-user"></i> {{ t('user_profile') }}</h2>
<div class="profile-info" id="profile-info">
<!-- Loading spinner -->
<div class="spinner"></div>
</div>
<form id="profile-form" class="settings-form" style="display: none;">
<div class="form-group">
<label for="username">{{ t('username') }}</label>
<input type="text" id="username" disabled>
<small>{{ t('username_readonly') }}</small>
</div>
<div class="form-group">
<label for="full_name">{{ t('full_name') }}</label>
<input type="text" id="full_name" maxlength="100" placeholder="{{ t('placeholder_full_name') }}">
</div>
<div class="form-group">
<label for="email">{{ t('email') }}</label>
<input type="email" id="email" placeholder="{{ t('placeholder_email') }}">
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ t('save_changes') }}
</button>
</form>
</div>
<div class="settings-section">
<h2><i class="fa-solid fa-lock"></i> {{ t('change_password') }}</h2>
<form id="password-form" class="settings-form">
<div class="form-group">
<label for="current_password">{{ t('current_password') }}</label>
<input type="password" id="current_password" required>
</div>
<div class="form-group">
<label for="new_password">{{ t('new_password') }}</label>
<input type="password" id="new_password" required minlength="6">
<small>{{ t('password_min_length') }}</small>
</div>
<div class="form-group">
<label for="confirm_password">{{ t('confirm_password') }}</label>
<input type="password" id="confirm_password" required minlength="6">
</div>
<button type="submit" class="btn btn-warning">
<i class="fas fa-key"></i> {{ t('change_password') }}
</button>
</form>
</div>
<div class="settings-section">
<h2><i class="fa-solid fa-key"></i> {{ t('ssh_public_key') }}</h2>
<form id="ssh-key-form" class="settings-form">
<div class="form-group">
<label for="ssh_public_key">{{ t('ssh_public_key_label') }}</label>
<textarea id="ssh_public_key" rows="4" placeholder="ssh-rsa AAAAB3NzaC1yc2EAAA... user@hostname" style="font-family: monospace; font-size: 0.85rem;"></textarea>
<small>{{ t('ssh_public_key_help') }}</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ t('save_ssh_key') }}
</button>
</form>
</div>
</div>
<!-- Notifications Settings Tab -->
<div id="notifications-tab" class="tab-content">
<!-- Admin Channel Settings Section -->
<div class="settings-section admin-only-section" id="channel-settings-section" style="display: none;">
<h2><i class="fas fa-cogs"></i> {{ t('notification_channel_config') or 'Настройка каналов уведомлений' }}</h2>
<!-- SMTP Settings -->
<div class="channel-config-card">
<div class="channel-config-header">
<i class="fas fa-envelope" style="color: var(--warning);"></i>
<span>SMTP (Email)</span>
</div>
<form id="smtp-settings-form" class="settings-form">
<div class="form-row">
<div class="form-group" style="flex: 2;">
<label for="smtp_host">{{ t('smtp_host') or 'SMTP сервер' }}</label>
<input type="text" id="smtp_host" placeholder="smtp.yandex.ru">
<small>Yandex: smtp.yandex.ru | Gmail: smtp.gmail.com | Mail.ru: smtp.mail.ru</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="smtp_port">{{ t('smtp_port') or 'Порт' }}</label>
<input type="number" id="smtp_port" placeholder="465" value="465">
<small>465 (SSL) или 587 (TLS)</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1;">
<label for="smtp_user">{{ t('smtp_user') or 'Пользователь' }}</label>
<input type="text" id="smtp_user" placeholder="user@example.com">
</div>
<div class="form-group" style="flex: 1;">
<label for="smtp_password">{{ t('smtp_password') or 'Пароль' }}</label>
<input type="password" id="smtp_password" placeholder="••••••••">
<small>{{ t('smtp_password_help') or 'Используйте пароль приложения' }}</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 2;">
<label for="smtp_from">{{ t('smtp_from') or 'Email отправителя' }}</label>
<input type="email" id="smtp_from" placeholder="noreply@example.com">
</div>
<div class="form-group" style="flex: 1;">
<label class="toggle-label" style="margin-top: 25px;">
<input type="checkbox" id="smtp_tls" checked>
<span class="toggle-text">TLS/SSL</span>
</label>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ t('save') or 'Сохранить' }}
</button>
<button type="button" id="test-smtp-settings-btn" class="btn btn-secondary">
<i class="fas fa-paper-plane"></i> {{ t('test_connection') or 'Тест' }}
</button>
</div>
</form>
</div>
<!-- Telegram Settings -->
<div class="channel-config-card" style="margin-top: 20px;">
<div class="channel-config-header">
<i class="fab fa-telegram" style="color: #0088cc;"></i>
<span>Telegram Bot</span>
</div>
<form id="telegram-settings-form" class="settings-form">
<div class="form-group">
<label for="telegram_bot_token">{{ t('telegram_token') or 'Токен бота' }}</label>
<input type="password" id="telegram_bot_token" placeholder="123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ">
<small>{{ t('telegram_token_help') or 'Получите токен у @BotFather в Telegram' }}</small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ t('save') or 'Сохранить' }}
</button>
<button type="button" id="test-telegram-settings-btn" class="btn btn-secondary">
<i class="fas fa-paper-plane"></i> {{ t('test_connection') or 'Тест' }}
</button>
</div>
</form>
</div>
</div>
<div class="settings-section">
<h2><i class="fas fa-bell"></i> {{ t('notification_channels') }}</h2>
<div id="channels-status" class="info-grid">
<div class="spinner"></div>
</div>
</div>
<div class="settings-section">
<h2><i class="fas fa-sliders-h"></i> {{ t('notification_settings') }}</h2>
<form id="notification-settings-form" class="settings-form">
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="notify_email_enabled">
<span class="toggle-text"><i class="fas fa-envelope"></i> {{ t('email_notifications') }}</span>
</label>
<small>{{ t('email_notifications_help') }}</small>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="notify_telegram_enabled">
<span class="toggle-text"><i class="fab fa-telegram"></i> {{ t('telegram_notifications') }}</span>
</label>
<small>{{ t('telegram_notifications_help') }}</small>
</div>
<div class="form-group" id="telegram-chat-id-group" style="display: none;">
<label for="telegram_chat_id">{{ t('telegram_chat_id') }}</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="telegram_chat_id" placeholder="123456789" style="flex: 1;">
<button type="button" id="verify-telegram-btn" class="btn btn-secondary" style="white-space: nowrap; flex-shrink: 0; padding: 10px 16px;">
<i class="fas fa-check-circle"></i> {{ t('verify') }}
</button>
</div>
<small>{{ t('telegram_chat_id_help') }} <a href="https://t.me/userinfobot" target="_blank" style="color: var(--info);">@userinfobot</a></small>
</div>
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" id="notify_critical_only">
<span class="toggle-text"><i class="fas fa-exclamation-triangle"></i> {{ t('critical_only') }}</span>
</label>
<small>{{ t('critical_only_help') }}</small>
</div>
<div class="form-group">
<label>{{ t('quiet_hours') }}</label>
<div style="display: flex; gap: 15px; align-items: center;">
<input type="time" id="quiet_hours_start" style="width: auto;">
<span style="color: var(--text-secondary);">{{ t('to') }}</span>
<input type="time" id="quiet_hours_end" style="width: auto;">
</div>
<small>{{ t('quiet_hours_help') }}</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ t('save_settings') }}
</button>
</form>
</div>
<div class="settings-section">
<h2><i class="fas fa-vial"></i> {{ t('testing') }}</h2>
<div class="settings-actions" style="flex-wrap: wrap;">
<button id="test-inapp-btn" class="btn btn-secondary">
<i class="fas fa-desktop"></i> {{ t('test_inapp') }}
</button>
<button id="test-email-btn" class="btn btn-secondary">
<i class="fas fa-envelope"></i> {{ t('test_email') }}
</button>
<button id="test-telegram-btn" class="btn btn-secondary">
<i class="fab fa-telegram"></i> {{ t('test_telegram') }}
</button>
<button id="test-all-btn" class="btn btn-primary">
<i class="fas fa-paper-plane"></i> {{ t('test_all_channels') }}
</button>
</div>
</div>
</div>
<!-- Panel Settings Tab -->
<div id="panel-tab" class="tab-content">
<div class="settings-section">
<h2><i class="fa-solid fa-desktop"></i> {{ t('panel_info') }}</h2>
<div id="panel-info" class="info-grid">
<!-- Loading spinner -->
<div class="spinner"></div>
</div>
</div>
<div class="settings-section" id="panel-settings-form-section">
<h2><i class="fa-solid fa-gear"></i> {{ t('panel_settings') }}</h2>
<form id="panel-form" class="settings-form">
<div class="form-group">
<label for="refresh_interval">{{ t('refresh_interval_label') }}</label>
<input type="number" id="refresh_interval" min="1" max="60" value="5">
<small>{{ t('refresh_interval_help') }}</small>
</div>
<div class="form-group">
<label for="log_retention_days">{{ t('log_retention_label') }}</label>
<input type="number" id="log_retention_days" min="1" max="365" value="30">
<small>{{ t('log_retention_help') }}</small>
</div>
<div class="form-group">
<label for="language">{{ t('language') }}</label>
<select id="language">
<option value="ru">🇷🇺 Русский</option>
<option value="en">🇺🇸 English (US)</option>
</select>
<small>{{ t('language_help') }}</small>
</div>
<!-- Session Settings (for administrators only) -->
<div class="form-group admin-only-section" id="session-settings-section" style="display: none;">
<label class="toggle-label">
<input type="checkbox" id="single_session_enabled">
<span class="toggle-text"><i class="fas fa-user-lock"></i> {{ t('single_session_mode') or 'Режим одной сессии' }}</span>
</label>
<small>{{ t('single_session_help') or 'Если включено, пользователь может быть авторизован только на одном устройстве. При входе с нового устройства старая сессия будет завершена.' }}</small>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ t('save_settings') }}
</button>
</form>
<div class="settings-actions" style="margin-top: 20px;">
<button id="cleanup-logs-btn" class="btn btn-danger">
<i class="fas fa-trash"></i> {{ t('cleanup_logs') }}
</button>
</div>
</div>
<!-- System Updates Section -->
<div class="settings-section admin-only-section" id="system-updates-section">
<h2><i class="fa-solid fa-cloud-arrow-down"></i> {{ t('system_updates') }}</h2>
<!-- Repository Selection -->
<div class="form-group">
<label for="git-repository-select">
<i class="fab fa-git-alt"></i> {{ t('git_repository') }}
</label>
<select id="git-repository-select" class="form-control" onchange="handleRepositoryChange()">
<option value="https://git.tzim.uz/markmorado/pvemanager">git.tzim.uz/markmorado/pvemanager (Default)</option>
<option value="https://github.com/markmorado/pvemanager">github.com/markmorado/pvemanager</option>
<option value="custom">{{ t('custom_repository') }}...</option>
</select>
<small class="form-text">{{ t('repository_url') }}</small>
</div>
<div id="custom-repo-input" class="form-group" style="display: none; margin-top: 10px;">
<input type="text" id="custom-repository-url" class="form-control"
placeholder="https://git.example.com/owner/repo">
<button class="btn btn-primary mt-2" onclick="saveCustomRepository()">
<i class="fas fa-save"></i> {{ t('save') }}
</button>
</div>
<div class="update-info-card">
<div class="update-version-row">
<div class="version-item">
<span class="version-label">{{ t('current_version') }}</span>
<span class="version-value" id="current-version">-</span>
</div>
<div class="version-item">
<span class="version-label">{{ t('latest_version') }}</span>
<span class="version-value" id="latest-version">-</span>
</div>
<div class="version-item" id="commits-behind-item" style="display: none;">
<span class="version-label">{{ t('commits_behind') }}</span>
<span class="version-value text-warning" id="commits-behind">0</span>
</div>
</div>
<div id="update-status-box" class="update-status-box" style="display: none;">
<div id="update-status-message"></div>
</div>
<div id="update-changelog-box" class="update-changelog-box" style="display: none;">
<h4><i class="fas fa-list"></i> {{ t('update_changelog') }}</h4>
<div id="update-changelog-content" class="changelog-content"></div>
</div>
<div class="update-actions">
<button id="check-updates-btn" class="btn btn-secondary" onclick="checkForUpdates()">
<i class="fas fa-sync-alt"></i> {{ t('check_updates') }}
</button>
<button id="update-now-btn" class="btn btn-success" style="display: none;" onclick="performUpdate()">
<i class="fas fa-download"></i> {{ t('update_now') }}
</button>
</div>
<small class="update-warning">
<i class="fas fa-exclamation-triangle"></i> {{ t('update_warning') }}
</small>
</div>
</div>
</div>
</div>
<style>
.settings-container {
max-width: 900px;
margin: 0 auto;
}
.settings-container h1 {
margin-bottom: var(--spacing-xl);
color: var(--text-primary);
}
/* Tabs Navigation */
.tabs-nav {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xl);
padding: var(--spacing-xs);
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
flex-wrap: wrap;
}
.tab-btn {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: 12px 20px;
background: transparent;
border: none;
border-radius: var(--radius-lg);
color: var(--text-secondary);
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all var(--transition-normal);
white-space: nowrap;
}
.tab-btn:hover {
color: var(--text-primary);
background: var(--bg-surface-hover);
}
.tab-btn.active {
color: var(--text-on-primary);
background: var(--gradient-primary);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.35);
}
.tab-btn i {
font-size: 16px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: fadeIn 0.3s;
}
@keyframes settingsFadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.settings-section {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
box-shadow: var(--shadow-lg);
animation: settingsFadeIn 0.4s ease-out;
}
.settings-section h2 {
margin: 0 0 var(--spacing-lg) 0;
color: var(--text-primary);
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.settings-section h2 i {
color: var(--primary);
}
.settings-form {
max-width: 600px;
}
.form-group {
margin-bottom: var(--spacing-lg);
}
.form-group label {
display: block;
margin-bottom: var(--spacing-sm);
color: var(--text-primary);
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px 16px;
background: var(--bg-input);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 14px;
transition: all var(--transition-fast);
}
.form-group input:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--bg-surface-hover);
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.form-group small {
display: block;
margin-top: var(--spacing-xs);
color: var(--text-muted);
font-size: 12px;
}
.form-group small a {
color: var(--primary);
}
.profile-info {
margin-bottom: var(--spacing-lg);
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-md);
}
.info-item {
padding: var(--spacing-md);
background: var(--bg-surface-hover);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
}
.info-item label {
display: block;
color: var(--text-muted);
font-size: 11px;
margin-bottom: var(--spacing-xs);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-item .value {
color: var(--text-primary);
font-size: 15px;
font-weight: 500;
}
.info-item .info-label {
color: var(--text-muted);
font-size: 12px;
margin-bottom: 4px;
}
.info-item .info-value {
color: var(--text-primary);
font-size: 15px;
font-weight: 500;
}
.info-item .badge {
display: inline-block;
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 600;
}
.badge.admin {
background: var(--danger-light);
color: var(--danger);
}
.badge.user {
background: var(--success-light);
color: var(--success);
}
.badge.active {
background: var(--success-light);
color: var(--success);
}
.badge.inactive {
background: var(--bg-surface-hover);
color: var(--text-muted);
}
.settings-actions {
display: flex;
gap: var(--spacing-sm);
flex-wrap: wrap;
}
/* Override button styles for settings page */
.settings-section .btn {
padding: 10px 20px;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all var(--transition-fast);
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
}
.settings-section .btn i {
margin-right: 0;
}
.settings-section .btn-primary {
background: var(--gradient-primary);
color: var(--text-on-primary);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.settings-section .btn-primary:hover {
filter: brightness(1.1);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.settings-section .btn-warning {
background: linear-gradient(135deg, var(--warning) 0%, #ea580c 100%);
color: var(--text-on-warning);
box-shadow: 0 4px 15px rgba(249, 115, 22, 0.4);
}
.settings-section .btn-warning:hover {
filter: brightness(1.1);
box-shadow: 0 6px 20px rgba(249, 115, 22, 0.5);
}
.settings-section .btn-danger {
background: linear-gradient(135deg, var(--danger) 0%, #dc2626 100%);
color: var(--text-on-danger);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.4);
}
.settings-section .btn-danger:hover {
filter: brightness(1.1);
box-shadow: 0 6px 20px rgba(239, 68, 68, 0.5);
}
.settings-section .btn-secondary {
background: var(--bg-surface-hover);
color: var(--text-primary);
border: 1px solid var(--border-default);
box-shadow: none;
}
.settings-section .btn-secondary:hover {
background: var(--bg-surface-active);
}
.settings-section .btn-success {
background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
color: var(--text-on-success);
box-shadow: 0 4px 15px rgba(16, 185, 129, 0.4);
}
.settings-section .btn-success:hover {
filter: brightness(1.1);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.5);
}
/* System Updates Styles */
.update-info-card {
background: var(--bg-surface);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
}
.update-version-row {
display: flex;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-lg);
flex-wrap: wrap;
}
.version-item {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.version-label {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.version-value {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.version-value.text-success {
color: var(--success);
}
.version-value.text-warning {
color: var(--warning);
}
.update-status-box {
padding: var(--spacing-md);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
}
.update-status-box.info {
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.3);
color: #3b82f6;
}
.update-status-box.success {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.3);
color: var(--success);
}
.update-status-box.error {
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
color: var(--danger);
}
.update-status-box.warning {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
color: #f59e0b;
}
.update-changelog-box {
margin-bottom: var(--spacing-lg);
}
.update-changelog-box h4 {
margin: 0 0 var(--spacing-sm) 0;
font-size: 14px;
color: var(--text-secondary);
}
.changelog-content {
background: var(--bg-input);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
padding: var(--spacing-md);
max-height: 200px;
overflow-y: auto;
font-size: 13px;
line-height: 1.6;
color: var(--text-secondary);
white-space: pre-wrap;
font-family: var(--font-mono);
}
.update-actions {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.update-warning {
display: block;
color: var(--warning);
font-size: 12px;
}
.update-warning i {
margin-right: 4px;
}
.toggle-label {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
}
.toggle-label input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: var(--primary);
}
.toggle-text {
color: var(--text-primary);
font-weight: 500;
}
.toggle-text i {
margin-right: 8px;
width: 20px;
color: var(--primary);
}
.channel-card {
padding: var(--spacing-md);
background: var(--bg-surface-hover);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
display: flex;
align-items: center;
gap: var(--spacing-md);
min-width: 0;
transition: all var(--transition-fast);
}
.channel-card:hover {
filter: brightness(1.02);
box-shadow: var(--shadow-md);
}
.channel-card.configured {
border-color: var(--success);
}
.channel-card.not-configured {
border-color: var(--danger);
opacity: 0.7;
}
.channel-icon {
font-size: 24px;
width: 40px;
min-width: 40px;
text-align: center;
}
.channel-icon.email { color: var(--warning); }
.channel-icon.telegram { color: #0088cc; }
.channel-icon.inapp { color: var(--success); }
.channel-info {
flex: 1;
min-width: 0;
overflow: hidden;
}
.channel-name {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.channel-status {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.channel-badge {
padding: 4px 10px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
white-space: nowrap;
flex-shrink: 0;
}
.channel-badge.active {
background: var(--success-light);
color: var(--success);
}
.channel-badge.inactive {
background: var(--danger-light);
color: var(--danger);
}
/* Channel configuration cards */
.channel-config-card {
background: var(--bg-surface-hover);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
}
.channel-config-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--border-default);
}
.channel-config-header i {
font-size: 20px;
}
.form-row {
display: flex;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.form-row .form-group {
min-width: 150px;
}
.form-actions {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
flex-wrap: wrap;
}
.spinner {
border: 3px solid var(--border-default);
border-top: 3px solid var(--primary);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 20px auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Notification styles */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: var(--radius-md);
color: var(--text-on-primary);
font-weight: 500;
z-index: 10000;
animation: notificationSlideIn 0.3s ease-out;
box-shadow: var(--shadow-lg);
backdrop-filter: blur(10px);
}
@keyframes notificationSlideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
.notification-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.notification-error {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
.notification-info {
background: linear-gradient(135deg, #667eea 0%, #4f46e5 100%);
}
/* Responsive */
@media (max-width: 768px) {
.tabs-nav {
flex-direction: column;
gap: var(--spacing-xs);
padding: var(--spacing-sm);
}
.tab-btn {
width: 100%;
justify-content: flex-start;
}
.settings-section {
padding: var(--spacing-md);
}
.info-grid {
grid-template-columns: 1fr;
}
.settings-actions {
flex-direction: column;
}
.settings-actions .btn {
width: 100%;
justify-content: center;
}
.form-row {
flex-direction: column;
gap: var(--spacing-sm);
}
.form-group {
width: 100% !important;
flex: none !important;
}
.channel-config-card {
padding: var(--spacing-md);
}
}
@media (max-width: 480px) {
.settings-container {
padding: 0;
}
.settings-section h2 {
font-size: 1rem;
}
.btn {
font-size: 0.875rem;
padding: 0.5rem 1rem;
}
}
</style>
<script>
// Apply translations to dynamic content
function applyTranslations() {
// Update notifications and dynamic messages with translations
// This is called after loading panel settings
}
document.addEventListener('DOMContentLoaded', function() {
let currentUser = null;
let panelSettings = null;
let notificationPrefs = null;
// Tab switching
document.querySelectorAll('.tab-btn').forEach(button => {
button.addEventListener('click', function() {
const tabName = this.dataset.tab;
// Update buttons
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
// Update content
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(`${tabName}-tab`).classList.add('active');
// Load notifications data when tab is opened
if (tabName === 'notifications') {
loadChannelsStatus();
loadNotificationPrefs();
}
});
});
// Load profile
loadProfile();
// Load panel settings
loadPanelSettings();
// Load repository setting
loadRepositorySetting();
// Profile form
document.getElementById('profile-form').addEventListener('submit', async function(e) {
e.preventDefault();
const data = {
full_name: document.getElementById('full_name').value || null,
email: document.getElementById('email').value || null
};
try {
const response = await fetchWithAuth('/settings/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
const result = await response.json();
showNotification(t('profile_updated'), 'success');
loadProfile(); // Reload
} else {
const error = await response.json();
showNotification(error.detail || t('error_updating_profile'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_updating_profile'), 'error');
}
});
// Password form
document.getElementById('password-form').addEventListener('submit', async function(e) {
e.preventDefault();
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
showNotification(t('passwords_dont_match'), 'error');
return;
}
const data = {
current_password: document.getElementById('current_password').value,
new_password: newPassword,
confirm_password: confirmPassword
};
try {
const response = await fetchWithAuth('/settings/api/change-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
showNotification(t('password_changed'), 'success');
document.getElementById('password-form').reset();
} else {
const error = await response.json();
showNotification(error.detail || t('error_changing_password'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_changing_password'), 'error');
}
});
// SSH Key form
document.getElementById('ssh-key-form').addEventListener('submit', async function(e) {
e.preventDefault();
const sshKey = document.getElementById('ssh_public_key').value.trim();
// Basic validation
if (sshKey && !sshKey.match(/^(ssh-rsa|ssh-ed25519|ecdsa-sha2|ssh-dss)\s+\S+/)) {
showNotification(t('invalid_ssh_key_format'), 'error');
return;
}
const data = {
ssh_public_key: sshKey || null
};
try {
const response = await fetchWithAuth('/settings/api/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
showNotification(t('ssh_key_saved'), 'success');
} else {
const error = await response.json();
showNotification(error.detail || t('error_saving_ssh_key'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_saving_ssh_key'), 'error');
}
});
// Panel form
document.getElementById('panel-form').addEventListener('submit', async function(e) {
e.preventDefault();
const currentLang = document.getElementById('language').value;
const oldLang = panelSettings ? panelSettings.language : 'ru';
const data = {
refresh_interval: parseInt(document.getElementById('refresh_interval').value),
log_retention_days: parseInt(document.getElementById('log_retention_days').value),
language: currentLang
};
try {
const response = await fetchWithAuth('/settings/api/panel', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
// Also save session settings if admin
const sessionSection = document.getElementById('session-settings-section');
if (sessionSection && sessionSection.style.display !== 'none') {
const sessionData = {
single_session_enabled: document.getElementById('single_session_enabled').checked
};
await fetchWithAuth('/settings/api/security-settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sessionData)
});
}
// If language changed, reload page
if (currentLang !== oldLang) {
showNotification(t('language_changed_reload'), 'success');
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showNotification(t('panel_settings_updated'), 'success');
loadPanelSettings(); // Reload
}
} else {
const error = await response.json();
showNotification(error.detail || t('error_updating_settings'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_updating_settings'), 'error');
}
});
// Cleanup logs
document.getElementById('cleanup-logs-btn').addEventListener('click', async function() {
const confirmed = await showConfirm(
t('confirm_cleanup_logs'),
t('cleanup_logs'),
{ type: 'warning', confirmText: t('delete') || 'Delete', confirmClass: 'danger', icon: '<i class="fa-solid fa-trash"></i>' }
);
if (!confirmed) return;
const days = parseInt(document.getElementById('log_retention_days').value);
try {
const response = await fetchWithAuth(`/settings/api/cleanup-logs?days=${days}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
showNotification(result.message, 'success');
} else {
const error = await response.json();
showNotification(error.detail || t('error_cleanup_logs'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_cleanup_logs'), 'error');
}
});
async function loadProfile() {
try {
const response = await fetchWithAuth('/settings/api/profile');
if (!response) return; // Redirect happened
if (response.ok) {
currentUser = await response.json();
displayProfile(currentUser);
} else if (response.status === 403) {
// Forbidden - user doesn't have permission
showNotification(t('error_loading_profile'), 'error');
}
// For other errors, don't show notification (might be temporary)
} catch (error) {
// Network error - don't spam user with errors on page load
console.error('Error loading profile:', error);
}
}
function displayProfile(user) {
// Show admin sections in Panel tab if user is admin
if (user.is_admin) {
// Show admin-only sections in Panel tab
document.querySelectorAll('.admin-only-section').forEach(section => {
section.style.display = '';
});
// Load session settings for admin
loadSessionSettings();
// Load channel settings for admin
loadChannelSettings();
}
// Display read-only info
const profileInfo = document.getElementById('profile-info');
profileInfo.innerHTML = `
<div class="info-grid">
<div class="info-item">
<label>ID</label>
<div class="value">${user.id}</div>
</div>
<div class="info-item">
<label>${t('role')}</label>
<div class="value">
<span class="badge ${user.is_admin ? 'admin' : 'user'}">
${user.is_admin ? t('admin') : t('user')}
</span>
</div>
</div>
<div class="info-item">
<label>${t('status')}</label>
<div class="value">
<span class="badge ${user.is_active ? 'active' : 'inactive'}">
${user.is_active ? t('active') : t('inactive')}
</span>
</div>
</div>
<div class="info-item">
<label>${t('created_date')}</label>
<div class="value">${formatDate(user.created_at)}</div>
</div>
<div class="info-item">
<label>${t('last_login')}</label>
<div class="value">${formatDateTime(user.last_login)}</div>
</div>
</div>
`;
// Fill form
document.getElementById('username').value = user.username;
document.getElementById('full_name').value = user.full_name || '';
document.getElementById('email').value = user.email || '';
document.getElementById('ssh_public_key').value = user.ssh_public_key || '';
document.getElementById('profile-form').style.display = 'block';
}
async function loadPanelSettings() {
try {
const response = await fetchWithAuth('/settings/api/panel');
if (!response) return; // Redirect happened
if (response.ok) {
panelSettings = await response.json();
displayPanelSettings(panelSettings);
} else if (response.status === 403) {
// If forbidden, hide the settings form
document.getElementById('panel-settings-form-section').style.display = 'none';
displayPanelInfo(null);
}
// For other errors, don't show notification
} catch (error) {
// Network error - don't spam user with errors
console.error('Error loading panel settings:', error);
}
}
function displayPanelSettings(settings) {
displayPanelInfo(settings);
// Fill form (only for admins)
document.getElementById('refresh_interval').value = settings.refresh_interval;
document.getElementById('log_retention_days').value = settings.log_retention_days;
document.getElementById('language').value = settings.language || 'ru';
}
function displayPanelInfo(settings) {
const panelInfo = document.getElementById('panel-info');
if (!settings) {
panelInfo.innerHTML = `<p style="color: var(--text-secondary);">${t('loading')}</p>`;
return;
}
panelInfo.innerHTML = `
<div class="info-item">
<label>${t('refresh_interval')}</label>
<div class="value">${settings.refresh_interval} ${t('sec')}</div>
</div>
<div class="info-item">
<label>${t('log_retention_days')}</label>
<div class="value">${settings.log_retention_days} ${t('days')}</div>
</div>
<div class="info-item">
<label>${t('language')}</label>
<div class="value">${settings.language === 'en' ? '🇺🇸 English' : '🇷🇺 Русский'}</div>
</div>
`;
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 4px;
color: var(--text-on-primary, #fff);
font-weight: 500;
z-index: 10000;
animation: slideIn 0.3s;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
if (type === 'success') {
notification.style.background = 'linear-gradient(135deg, var(--success) 0%, var(--success-dark) 100%)';
} else if (type === 'error') {
notification.style.background = 'linear-gradient(135deg, var(--danger) 0%, var(--danger-dark) 100%)';
} else {
notification.style.background = 'linear-gradient(135deg, var(--purple) 0%, var(--purple-dark) 100%)';
}
document.body.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOut 0.3s';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// =====================
// Notification Settings
// =====================
async function loadChannelsStatus() {
try {
const response = await fetchWithAuth('/api/notifications/channels/status');
if (!response) return;
if (response.ok) {
const data = await response.json();
displayChannelsStatus(data);
}
} catch (error) {
console.error('Error loading channels status:', error);
}
}
function displayChannelsStatus(data) {
const container = document.getElementById('channels-status');
container.innerHTML = `
<div class="channel-card ${data.inapp.configured ? 'configured' : 'not-configured'}">
<div class="channel-icon inapp"><i class="fas fa-desktop"></i></div>
<div class="channel-info">
<div class="channel-name">${t('inapp_notifications')}</div>
<div class="channel-status">${t('browser_notifications')}</div>
</div>
<span class="channel-badge ${data.inapp.enabled ? 'active' : 'inactive'}">
${data.inapp.enabled ? t('enabled') : t('disabled')}
</span>
</div>
<div class="channel-card ${data.email.configured ? 'configured' : 'not-configured'}">
<div class="channel-icon email"><i class="fas fa-envelope"></i></div>
<div class="channel-info">
<div class="channel-name">Email</div>
<div class="channel-status">${data.email.configured ? t('smtp_configured') : t('smtp_not_configured')}</div>
</div>
<span class="channel-badge ${data.email.configured ? 'active' : 'inactive'}">
${data.email.configured ? t('available') : t('unavailable')}
</span>
</div>
<div class="channel-card ${data.telegram.configured ? 'configured' : 'not-configured'}">
<div class="channel-icon telegram"><i class="fab fa-telegram"></i></div>
<div class="channel-info">
<div class="channel-name">Telegram</div>
<div class="channel-status">${data.telegram.configured ? (data.telegram.bot_info || t('bot_configured')) : t('bot_not_configured')}</div>
</div>
<span class="channel-badge ${data.telegram.configured ? 'active' : 'inactive'}">
${data.telegram.configured ? t('available') : t('unavailable')}
</span>
</div>
`;
}
async function loadNotificationPrefs() {
try {
const response = await fetchWithAuth('/api/notifications/preferences');
if (!response) return;
if (response.ok) {
notificationPrefs = await response.json();
displayNotificationPrefs(notificationPrefs);
}
} catch (error) {
console.error('Error loading notification preferences:', error);
}
}
function displayNotificationPrefs(prefs) {
document.getElementById('notify_email_enabled').checked = prefs.email_enabled;
document.getElementById('notify_telegram_enabled').checked = prefs.telegram_enabled;
document.getElementById('notify_critical_only').checked = prefs.email_critical_only;
document.getElementById('telegram_chat_id').value = prefs.telegram_chat_id || '';
document.getElementById('quiet_hours_start').value = prefs.quiet_hours_start || '';
document.getElementById('quiet_hours_end').value = prefs.quiet_hours_end || '';
// Show/hide telegram chat ID field
toggleTelegramChatId();
}
function toggleTelegramChatId() {
const enabled = document.getElementById('notify_telegram_enabled').checked;
document.getElementById('telegram-chat-id-group').style.display = enabled ? 'block' : 'none';
}
document.getElementById('notify_telegram_enabled').addEventListener('change', toggleTelegramChatId);
// Notification settings form
document.getElementById('notification-settings-form').addEventListener('submit', async function(e) {
e.preventDefault();
const data = {
email_enabled: document.getElementById('notify_email_enabled').checked,
telegram_enabled: document.getElementById('notify_telegram_enabled').checked,
telegram_chat_id: document.getElementById('telegram_chat_id').value || null,
email_critical_only: document.getElementById('notify_critical_only').checked,
quiet_hours_start: document.getElementById('quiet_hours_start').value || null,
quiet_hours_end: document.getElementById('quiet_hours_end').value || null
};
try {
const response = await fetchWithAuth('/api/notifications/preferences', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
showNotification(t('notification_settings_saved'), 'success');
loadChannelsStatus();
} else {
const error = await response.json();
showNotification(error.detail || t('error_saving_settings'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_saving_settings'), 'error');
}
});
// Verify Telegram
document.getElementById('verify-telegram-btn').addEventListener('click', async function() {
const chatId = document.getElementById('telegram_chat_id').value;
if (!chatId) {
showNotification(t('enter_chat_id'), 'error');
return;
}
try {
const response = await fetchWithAuth(`/api/notifications/telegram/verify?chat_id=${chatId}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
showNotification(result.message || t('telegram_connected'), 'success');
} else {
const error = await response.json();
showNotification(error.detail || t('telegram_verify_error'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('telegram_verify_error'), 'error');
}
});
// Test notifications
async function testNotification(channel) {
// Show loading state
const btn = document.getElementById(`test-${channel}-btn`);
const originalText = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + t('testing');
}
try {
const response = await fetchWithAuth(`/api/notifications/test?channel=${channel}`, {
method: 'POST'
});
// Check if response exists (fetchWithAuth returns undefined on redirect)
if (!response) {
return;
}
if (response.ok) {
const result = await response.json();
let message = t('test_completed') + ': ';
let hasSuccess = false;
if (result.inapp) { message += '✓ In-App '; hasSuccess = true; }
if (result.email) { message += '✓ Email '; hasSuccess = true; }
if (result.telegram) { message += '✓ Telegram '; hasSuccess = true; }
if (result.errors && result.errors.length > 0) {
message += '\n' + t('errors') + ': ' + result.errors.join(', ');
}
showNotification(message, result.errors && result.errors.length > 0 ? 'warning' : (hasSuccess ? 'success' : 'error'));
} else {
const error = await response.json();
showNotification(error.detail || t('test_error'), 'error');
}
} catch (error) {
console.error('Test notification error:', error);
showNotification(t('test_error') + ': ' + error.message, 'error');
} finally {
// Restore button state
if (btn) {
btn.disabled = false;
btn.innerHTML = originalText;
}
}
}
document.getElementById('test-inapp-btn').addEventListener('click', () => testNotification('inapp'));
document.getElementById('test-email-btn').addEventListener('click', () => testNotification('email'));
document.getElementById('test-telegram-btn').addEventListener('click', () => testNotification('telegram'));
document.getElementById('test-all-btn').addEventListener('click', () => testNotification('all'));
// ==================== Session Settings ====================
// Load session settings on admin tab
async function loadSessionSettings() {
try {
const response = await fetchWithAuth('/settings/api/security-settings');
if (response && response.ok) {
const settings = await response.json();
document.getElementById('single_session_enabled').checked = settings.single_session_enabled || false;
}
} catch (error) {
console.error('Error loading session settings:', error);
}
}
// ==================== Notification Channel Settings ====================
async function loadChannelSettings() {
try {
const response = await fetchWithAuth('/settings/api/notification-channels');
if (!response || !response.ok) return;
const data = await response.json();
// SMTP settings
document.getElementById('smtp_host').value = data.smtp.host || '';
document.getElementById('smtp_port').value = data.smtp.port || 587;
document.getElementById('smtp_user').value = data.smtp.user || '';
document.getElementById('smtp_from').value = data.smtp.from || '';
document.getElementById('smtp_tls').checked = data.smtp.tls !== false;
// Don't fill password field if it's masked
if (data.smtp.password && data.smtp.password !== '***') {
document.getElementById('smtp_password').value = data.smtp.password;
}
// Telegram settings - don't show masked token
if (data.telegram.bot_token && data.telegram.bot_token !== '***') {
document.getElementById('telegram_bot_token').value = data.telegram.bot_token;
}
} catch (error) {
console.error('Error loading channel settings:', error);
}
}
// SMTP settings form
document.getElementById('smtp-settings-form').addEventListener('submit', async function(e) {
e.preventDefault();
const password = document.getElementById('smtp_password').value;
const data = {
smtp_host: document.getElementById('smtp_host').value || null,
smtp_port: parseInt(document.getElementById('smtp_port').value) || 587,
smtp_user: document.getElementById('smtp_user').value || null,
smtp_from: document.getElementById('smtp_from').value || null,
smtp_tls: document.getElementById('smtp_tls').checked
};
// Only update password if it was changed (not empty)
if (password) {
data.smtp_password = password;
}
try {
const response = await fetchWithAuth('/settings/api/notification-channels/smtp', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response && response.ok) {
showNotification(t('smtp_settings_saved') || 'Настройки SMTP сохранены', 'success');
loadChannelsStatus();
} else if (response) {
const error = await response.json();
showNotification(error.detail || t('error_saving_settings'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_saving_settings'), 'error');
}
});
// Telegram settings form
document.getElementById('telegram-settings-form').addEventListener('submit', async function(e) {
e.preventDefault();
const token = document.getElementById('telegram_bot_token').value;
if (!token) {
showNotification(t('enter_bot_token') || 'Введите токен бота', 'error');
return;
}
try {
const response = await fetchWithAuth('/settings/api/notification-channels/telegram', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ telegram_bot_token: token })
});
if (response && response.ok) {
showNotification(t('telegram_settings_saved') || 'Настройки Telegram сохранены', 'success');
loadChannelsStatus();
} else if (response) {
const error = await response.json();
showNotification(error.detail || t('error_saving_settings'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('error_saving_settings'), 'error');
}
});
// Test SMTP settings
document.getElementById('test-smtp-settings-btn').addEventListener('click', async function() {
const testEmail = prompt(t('enter_test_email') || 'Введите email для тестового письма:', currentUser?.email || '');
if (!testEmail) return;
const btn = this;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + (t('sending') || 'Отправка...');
try {
const response = await fetchWithAuth(`/settings/api/notification-channels/smtp/test?test_email=${encodeURIComponent(testEmail)}`, {
method: 'POST'
});
if (response && response.ok) {
const result = await response.json();
showNotification(result.message || t('test_email_sent'), 'success');
} else if (response) {
const error = await response.json();
showNotification(error.detail || t('test_email_error'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('test_email_error') || 'Ошибка отправки тестового письма', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-paper-plane"></i> ' + (t('test_connection') || 'Тест');
}
});
// Test Telegram settings
document.getElementById('test-telegram-settings-btn').addEventListener('click', async function() {
const chatId = prompt(t('enter_chat_id_for_test') || 'Введите Chat ID для тестового сообщения:');
if (!chatId) return;
const btn = this;
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + (t('sending') || 'Отправка...');
try {
const response = await fetchWithAuth(`/settings/api/notification-channels/telegram/test?chat_id=${encodeURIComponent(chatId)}`, {
method: 'POST'
});
if (response && response.ok) {
const result = await response.json();
showNotification(result.message || t('test_telegram_sent'), 'success');
} else if (response) {
const error = await response.json();
showNotification(error.detail || t('test_telegram_error'), 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification(t('test_telegram_error') || 'Ошибка отправки тестового сообщения', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-paper-plane"></i> ' + (t('test_connection') || 'Тест');
}
});
// Load current version on page load
loadCurrentVersion();
});
// ==================== System Updates ====================
async function loadCurrentVersion() {
try {
const response = await fetchWithAuth('/settings/api/version');
if (response && response.ok) {
const data = await response.json();
document.getElementById('current-version').textContent = data.version;
}
} catch (error) {
console.error('Error loading version:', error);
}
}
// Load current repository setting
async function loadRepositorySetting() {
try {
const response = await fetchWithAuth('/settings/api/updates/repository');
if (response && response.ok) {
const data = await response.json();
const select = document.getElementById('git-repository-select');
const repoUrl = data.repository_url;
// Check if it's one of the predefined options
let found = false;
for (let option of select.options) {
if (option.value === repoUrl) {
select.value = repoUrl;
found = true;
break;
}
}
// If not found, it's a custom repository
if (!found && repoUrl !== 'custom') {
select.value = 'custom';
document.getElementById('custom-repo-input').style.display = 'block';
document.getElementById('custom-repository-url').value = repoUrl;
}
}
} catch (error) {
console.error('Error loading repository setting:', error);
}
}
// Handle repository selection change
async function handleRepositoryChange() {
const select = document.getElementById('git-repository-select');
const customInput = document.getElementById('custom-repo-input');
const value = select.value;
if (value === 'custom') {
customInput.style.display = 'block';
return;
}
customInput.style.display = 'none';
// Save the selected repository
try {
const response = await fetchWithAuth('/settings/api/updates/repository', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(value)
});
if (response && response.ok) {
showNotification(t('repository_saved') || 'Репозиторий сохранён', 'success');
}
} catch (error) {
console.error('Error saving repository:', error);
showNotification(t('error_saving') || 'Ошибка сохранения', 'error');
}
}
// Save custom repository URL
async function saveCustomRepository() {
const input = document.getElementById('custom-repository-url');
const url = input.value.trim();
if (!url) {
showNotification(t('enter_repository_url') || 'Введите URL репозитория', 'error');
return;
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
showNotification('URL должен начинаться с http:// или https://', 'error');
return;
}
try {
const response = await fetchWithAuth('/settings/api/updates/repository', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(url)
});
if (response && response.ok) {
showNotification(t('repository_saved') || 'Репозиторий сохранён', 'success');
}
} catch (error) {
console.error('Error saving custom repository:', error);
showNotification(t('error_saving') || 'Ошибка сохранения', 'error');
}
}
async function checkForUpdates() {
const btn = document.getElementById('check-updates-btn');
const updateBtn = document.getElementById('update-now-btn');
const statusBox = document.getElementById('update-status-box');
const changelogBox = document.getElementById('update-changelog-box');
const commitsItem = document.getElementById('commits-behind-item');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + (t('checking_updates') || 'Проверка...');
try {
const response = await fetchWithAuth('/settings/api/updates/check');
if (response && response.ok) {
const data = await response.json();
// Update version display
document.getElementById('current-version').textContent = data.current_version;
document.getElementById('latest-version').textContent = data.latest_version || '-';
// Show commits behind if available
if (data.commits_behind > 0) {
document.getElementById('commits-behind').textContent = data.commits_behind;
commitsItem.style.display = 'flex';
} else {
commitsItem.style.display = 'none';
}
if (data.error) {
statusBox.style.display = 'block';
// Если проверка отключена, показываем warning вместо ошибки
if (data.disabled || data.error.includes('disabled')) {
statusBox.className = 'update-status-box warning';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-info-circle"></i> ' + (t('update_check_disabled') || 'Проверка обновлений отключена');
} else {
statusBox.className = 'update-status-box error';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-exclamation-circle"></i> ' + data.error;
}
updateBtn.style.display = 'none';
changelogBox.style.display = 'none';
} else if (data.update_available) {
statusBox.style.display = 'block';
statusBox.className = 'update-status-box info';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-info-circle"></i> ' + (t('update_available') || 'Доступно обновление') +
': <strong>v' + data.latest_version + '</strong>';
updateBtn.style.display = 'inline-flex';
// Show changelog if available
if (data.changelog) {
changelogBox.style.display = 'block';
document.getElementById('update-changelog-content').textContent = data.changelog;
}
} else {
statusBox.style.display = 'block';
statusBox.className = 'update-status-box success';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-check-circle"></i> ' + (t('no_updates') || 'Установлена последняя версия');
updateBtn.style.display = 'none';
changelogBox.style.display = 'none';
}
} else if (response) {
const error = await response.json();
showNotification(error.detail || 'Error checking updates', 'error');
}
} catch (error) {
console.error('Error checking for updates:', error);
showNotification('Error checking for updates', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-sync-alt"></i> ' + (t('check_updates') || 'Проверить обновления');
}
}
async function performUpdate() {
// Confirm update
if (!confirm((t('update_warning') || 'Панель будет недоступна во время обновления') + '\n\nПродолжить?')) {
return;
}
const updateBtn = document.getElementById('update-now-btn');
const statusBox = document.getElementById('update-status-box');
updateBtn.disabled = true;
updateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> ' + (t('updating_system') || 'Обновление...');
// Store update state in localStorage for banner
localStorage.setItem('pve_update_in_progress', 'true');
localStorage.setItem('pve_update_started', Date.now().toString());
try {
const response = await fetchWithAuth('/settings/api/updates/perform', {
method: 'POST'
});
if (response && response.ok) {
statusBox.style.display = 'block';
statusBox.className = 'update-status-box info';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-spinner fa-spin"></i> ' + (t('update_in_progress') || 'Выполняется обновление') +
'... <br><small>Страница обновится автоматически после завершения.</small>';
// Start polling for server availability
pollForServerRestart();
} else if (response) {
const error = await response.json();
statusBox.style.display = 'block';
statusBox.className = 'update-status-box error';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-exclamation-circle"></i> ' + (error.error || error.detail || 'Update failed');
localStorage.removeItem('pve_update_in_progress');
localStorage.removeItem('pve_update_started');
updateBtn.disabled = false;
updateBtn.innerHTML = '<i class="fas fa-download"></i> ' + (t('update_now') || 'Обновить сейчас');
}
} catch (error) {
console.error('Error performing update:', error);
// This is expected if server restarts during update
pollForServerRestart();
}
}
async function pollForServerRestart() {
const statusBox = document.getElementById('update-status-box');
let attempts = 0;
const maxAttempts = 60; // 2 minutes (2s * 60)
async function checkServer() {
attempts++;
try {
const response = await fetch('/settings/api/version', {
method: 'GET',
cache: 'no-cache'
});
if (response.ok) {
// Server is back!
localStorage.removeItem('pve_update_in_progress');
localStorage.removeItem('pve_update_started');
statusBox.className = 'update-status-box success';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-check-circle"></i> ' + (t('update_success') || 'Обновление завершено успешно') +
'<br><small>Перезагрузка страницы...</small>';
// Reload page after short delay
setTimeout(() => {
window.location.reload();
}, 2000);
return;
}
} catch (e) {
// Server still down, continue polling
}
if (attempts < maxAttempts) {
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-spinner fa-spin"></i> ' + (t('update_in_progress') || 'Выполняется обновление') +
`... (${attempts}/${maxAttempts})`;
setTimeout(checkServer, 2000);
} else {
statusBox.className = 'update-status-box error';
statusBox.querySelector('#update-status-message').innerHTML =
'<i class="fas fa-exclamation-circle"></i> ' +
'Превышено время ожидания. Проверьте сервер вручную.';
localStorage.removeItem('pve_update_in_progress');
localStorage.removeItem('pve_update_started');
}
}
// Start polling after a short delay to allow server to start shutting down
setTimeout(checkServer, 3000);
}
</script>
{% endblock %}