feat: real-time WebSocket metrics broadcaster and UI updates

- New metrics_broadcaster service (410 lines) — background broadcaster
  pushing CPU/RAM/disk/network stats to subscribed WebSocket clients
- WebSocket protocol extended: subscribe/unsubscribe channels,
  get_rrd/get_node_rrd requests with request_id
- main.py: metrics broadcaster started in lifespan, WS message dispatcher
- websocket_manager.py: subscription map support
- templates: dashboard, instance_detail, proxmox_server_detail,
  proxmox_vms, workspaces — real-time charts and UI updates
- auth.py, servers.py, settings.py: minor fixes
This commit is contained in:
markmorado
2026-03-15 02:49:38 +01:00
parent c4778d2972
commit ed7f1f779e
14 changed files with 1203 additions and 167 deletions

View File

@@ -216,6 +216,26 @@ def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_
}
@router.post("/api/auth/refresh", response_model=Token)
def refresh_token(
current_user: User = Depends(get_current_user),
session_token: str = Depends(get_current_session_token),
db: Session = Depends(get_db)
):
"""Refresh JWT access token without re-login (extends session)"""
session_timeout = SecurityService.get_setting_int(db, "session_timeout_minutes", 60)
access_token_expires = timedelta(minutes=session_timeout)
new_token = create_access_token(
data={"sub": current_user.username},
session_token=session_token,
expires_delta=access_token_expires
)
return {
"access_token": new_token,
"token_type": "bearer"
}
@router.post("/api/auth/logout")
def logout(
request: Request,

View File

@@ -33,9 +33,11 @@ def vms_page(request: Request, db: Session = Depends(get_db)):
"""Страница управления Proxmox серверами, VM и LXC"""
from ...i18n import t
lang = request.cookies.get("language", "en")
# All servers are needed for cluster modals (admin operations).
# The main servers table is rendered dynamically via JS with workspace filtering.
proxmox_servers = db.query(ProxmoxServer).all()
context = {
"request": request,
"proxmox_servers": proxmox_servers,

View File

@@ -75,13 +75,11 @@ class ChangePasswordRequest(BaseModel):
class PanelSettingsResponse(BaseModel):
panel_name: str
refresh_interval: int # seconds
log_retention_days: int
language: str # ru or en
class UpdatePanelSettingsRequest(BaseModel):
refresh_interval: Optional[int] = Field(None, ge=1, le=60)
log_retention_days: Optional[int] = Field(None, ge=1, le=365)
language: Optional[str] = Field(None, pattern='^(ru|en)$')
@@ -269,13 +267,11 @@ async def get_panel_settings(
from ..config import settings
# Get settings from database or use defaults
refresh_interval = get_setting(db, "refresh_interval", "5")
log_retention_days = get_setting(db, "log_retention_days", "30")
language = get_setting(db, "language", "ru")
return {
"panel_name": settings.PANEL_NAME,
"refresh_interval": int(refresh_interval),
"log_retention_days": int(log_retention_days),
"language": language
}
@@ -291,10 +287,6 @@ async def update_panel_settings(
updated_fields = []
# Save settings to database
if data.refresh_interval is not None:
set_setting(db, "refresh_interval", str(data.refresh_interval), "Интервал обновления данных (секунды)")
updated_fields.append(f"refresh_interval={data.refresh_interval}")
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}")