feat(settings): per-user language preference and panel improvements

Store UI language per user (users.language column, migration 26).
Language selector moves from Panel tab to Profile tab — each account
picks its own language, saved to DB and applied via i18n.changeLanguage()
on save so the interface switches immediately.

Panel tab: panel_name read-only (falls back to env PANEL_NAME),
log_retention_days field exposed in UI, language selector removed.
Panel API accepts panel_name in PUT so it can be overridden in DB
without touching the env var.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 08:47:18 +05:00
parent 4451b3e45a
commit 9ced08b88c
6 changed files with 38 additions and 2 deletions

View File

@@ -62,6 +62,7 @@ class UpdateProfileRequest(BaseModel):
full_name: Optional[str] = Field(None, max_length=100)
email: Optional[EmailStr] = None
ssh_public_key: Optional[str] = Field(None, max_length=10000, description="SSH public key for VM/LXC deployment")
language: Optional[str] = Field(None, pattern='^(ru|en)$')
class ChangePasswordRequest(BaseModel):
@@ -77,6 +78,7 @@ class PanelSettingsResponse(BaseModel):
class UpdatePanelSettingsRequest(BaseModel):
panel_name: Optional[str] = Field(None, min_length=1, max_length=100)
log_retention_days: Optional[int] = Field(None, ge=1, le=365)
language: Optional[str] = Field(None, pattern='^(ru|en)$')
@@ -115,6 +117,7 @@ async def get_profile(
"is_admin": current_user.is_admin,
"is_active": current_user.is_active,
"ssh_public_key": current_user.ssh_public_key,
"language": current_user.language or 'ru',
"created_at": current_user.created_at.isoformat() if current_user.created_at else None,
"last_login": current_user.last_login.isoformat() if current_user.last_login else None
}
@@ -157,6 +160,10 @@ async def update_profile(
)
current_user.ssh_public_key = ssh_key if ssh_key else None
updated_fields.append("ssh_public_key")
if data.language is not None:
current_user.language = data.language
updated_fields.append("language")
if updated_fields:
db.commit()
@@ -241,11 +248,12 @@ async def get_panel_settings(
from ..config import settings
# Get settings from database or use defaults
panel_name = get_setting(db, "panel_name", settings.PANEL_NAME)
log_retention_days = get_setting(db, "log_retention_days", "30")
language = get_setting(db, "language", "ru")
return {
"panel_name": settings.PANEL_NAME,
"panel_name": panel_name,
"log_retention_days": int(log_retention_days),
"language": language
}
@@ -261,6 +269,10 @@ async def update_panel_settings(
updated_fields = []
# Save settings to database
if data.panel_name is not None:
set_setting(db, "panel_name", data.panel_name, "Название панели")
updated_fields.append(f"panel_name={data.panel_name}")
if data.log_retention_days is not None:
set_setting(db, "log_retention_days", str(data.log_retention_days), "Срок хранения логов (дни)")
updated_fields.append(f"log_retention_days={data.log_retention_days}")

View File

@@ -53,6 +53,9 @@ class User(Base):
# SSH Public Key for VM/LXC deployment
ssh_public_key = Column(Text, nullable=True)
# UI language preference (ru/en)
language = Column(String(10), nullable=False, server_default='ru')
# Security fields
failed_login_attempts = Column(Integer, default=0, nullable=False)

View File

@@ -1366,6 +1366,14 @@ def migrate_proxmox_task_progress(conn):
logger.info("✓ proxmox_tasks.progress already exists")
def migrate_user_language(conn):
"""Migration 26: Add 'language' column to users table (per-user UI language)."""
if add_column_if_not_exists(conn, 'users', 'language', "VARCHAR(10) DEFAULT 'ru'"):
logger.info("✓ Added language column to users")
else:
logger.info("✓ users.language already exists")
def run_all_migrations(engine, db_session=None):
"""
Run all migrations in order.
@@ -1580,6 +1588,14 @@ def run_all_migrations(engine, db_session=None):
logger.warning(f"Proxmox task progress migration: {e}")
conn.rollback()
# Migration 26: users.language column
try:
migrate_user_language(conn)
conn.commit()
except Exception as e:
logger.warning(f"User language migration: {e}")
conn.rollback()
logger.info("=" * 50)
logger.info("✓ All migrations completed successfully")
logger.info("=" * 50)

View File

@@ -41,6 +41,7 @@ interface ProfileResponse {
is_admin: boolean;
is_active: boolean;
ssh_public_key?: string;
language: string;
created_at: string;
last_login?: string;
}

View File

@@ -681,6 +681,8 @@
"session_timeout": "Session Timeout",
"seconds": "seconds",
"minutes": "minutes",
"days": "days",
"log_retention_days": "Log Retention",
"security_saved": "Security settings saved",
"version": "Version",
"check_updates": "Check for Updates",

View File

@@ -681,6 +681,8 @@
"session_timeout": "Таймаут сессии",
"seconds": "секунд",
"minutes": "минут",
"days": "дней",
"log_retention_days": "Хранение логов",
"security_saved": "Настройки безопасности сохранены",
"version": "Версия",
"check_updates": "Проверить обновления",