fix: frontend-backend integration — 14 issues resolved

- nginx: add proxy routes for /settings/, /templates/, /ipam/, /logs/, /admin/, /health
- dashboard: fix data format mismatch (servers[] not nodes[]/vms[])
- dashboard: add WebSocket subscription to dashboard_metrics for real-time updates
- dashboard: connect activity feed to /logs/api/logs endpoint
- dashboard: reduce polling to 15s with WS as primary update source
- graphs: fix rrddata response unwrap (backend returns {data:[],timeframe:''})
- graphs: add i18n translations for chart titles
- console: add xterm.js CSS import for proper terminal rendering
- console: add i18n translations (connecting, reconnect)
- workspace: add switcher dropdown in TopBar
- workspace: add edit dialog in WorkspacesPage
- workspace: create Zustand store with localStorage persistence
- workspace: add X-Active-Workspace header to API client
- sidebar: replace generic icon with actual favicon.svg logo
- instances: translate RowActionMenu and InstanceDetailPage strings
- locales: add graphs, console, workspace, instance tab keys to en/ru
This commit is contained in:
markmorado
2026-04-07 00:45:43 +02:00
parent 4546a798d0
commit 1d08c815a7
138 changed files with 17826 additions and 32024 deletions

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from loguru import logger
@@ -16,11 +15,8 @@ from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerRe
from ...proxmox_client import ProxmoxClient, get_proxmox_resources
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
from ...logging_service import LoggingService
from ...template_helpers import add_i18n_context
from ...ipam_service import IPAMService
templates = Jinja2Templates(directory="app/templates")
# ==================== Helper Functions for User Isolation ====================

View File

@@ -9,8 +9,7 @@ from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from loguru import logger
@@ -18,59 +17,11 @@ from ...db import get_db
from ...models import BackupJob, ProxmoxServer, User
from ...auth import PermissionChecker
from ...logging_service import LoggingService
from ...template_helpers import add_i18n_context
from ._helpers import _get_proxmox_client, templates
from ._helpers import _get_proxmox_client
router = APIRouter()
# ==================== UI Page ====================
@router.get("/api/backups/page", response_class=HTMLResponse, include_in_schema=False)
async def backups_page(
request: Request,
db: Session = Depends(get_db),
):
from ...i18n import t
from ...api.workspaces import get_workspace_server_ids
from ...auth import get_current_user_optional
lang = request.cookies.get("language", "ru")
# Workspace filtering (best-effort, page may be loaded unauthenticated)
server_ids = None
try:
token = request.cookies.get("access_token") or (
request.headers.get("Authorization", "").removeprefix("Bearer ").strip() or None
)
if token:
from ...auth import decode_access_token
from ...models import User as UserModel
payload = decode_access_token(token)
username = payload.get("sub")
if username:
user_obj = db.query(UserModel).filter(
UserModel.username == username, UserModel.is_active == True
).first()
if user_obj:
server_ids = get_workspace_server_ids(request, db, user_obj)
except Exception:
pass
servers_q = db.query(ProxmoxServer).filter(ProxmoxServer.is_online.is_(True))
if server_ids is not None:
servers_q = servers_q.filter(ProxmoxServer.id.in_(server_ids))
servers = servers_q.all()
context = {
"request": request,
"page_title": t("nav_backups", lang),
"servers": [{"id": s.id, "name": s.name} for s in servers],
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("backups.html", context)
# ==================== Storage Management ====================
@router.get("/api/backups/storages/{server_id}")

View File

@@ -1,34 +1,16 @@
from fastapi import APIRouter, Depends, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from loguru import logger
from ...db import get_db
from ...models import ProxmoxServer, User, IPAMNetwork
from ...auth import PermissionChecker
from ...template_helpers import add_i18n_context
from ._helpers import _get_proxmox_client, templates
from ._helpers import _get_proxmox_client
router = APIRouter()
# ==================== HTML Page ====================
@router.get("/networks", response_class=HTMLResponse, include_in_schema=False)
def networks_page(request: Request, db: Session = Depends(get_db)):
"""Страница управления сетями PVE (SDN + интерфейсы нод)"""
from ...i18n import t
lang = request.cookies.get("language", "en")
proxmox_servers = db.query(ProxmoxServer).all()
context = {
"request": request,
"proxmox_servers": proxmox_servers,
"page_title": t("nav_networks", lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("networks.html", context)
# ==================== Node list ====================
@router.get("/api/servers/{server_id}/nodes")

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from loguru import logger
@@ -16,72 +15,14 @@ from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerRe
from ...proxmox_client import ProxmoxClient, get_proxmox_resources
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
from ...logging_service import LoggingService
from ...template_helpers import add_i18n_context
from ...ipam_service import IPAMService
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
get_next_vmid, archive_and_delete_snapshots,
save_vm_instance, get_vm_instance, soft_delete_vm_instance,
templates)
save_vm_instance, get_vm_instance, soft_delete_vm_instance)
router = APIRouter()
# ==================== HTML Pages ====================
@router.get("/vms", response_class=HTMLResponse, include_in_schema=False)
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,
"page_title": t('nav_proxmox', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("proxmox_vms.html", context)
@router.get("/server/{server_id}", response_class=HTMLResponse, include_in_schema=False)
def server_detail_page(request: Request, server_id: int, db: Session = Depends(get_db)):
"""Страница детального просмотра VM/LXC конкретного Proxmox сервера"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox server not found")
context = {
"request": request,
"server": server,
"page_title": server.name,
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("proxmox_server_detail.html", context)
@router.get("/server/{server_id}/instance/{vmid}", response_class=HTMLResponse, include_in_schema=False)
def instance_detail_page(request: Request, server_id: int, vmid: int, type: str = "qemu", node: str = "", db: Session = Depends(get_db)):
"""Страница детального просмотра конкретной VM или LXC"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox server not found")
context = {
"request": request,
"server": server,
"vmid": vmid,
"type": type,
"node": node,
"page_title": f"VM {vmid}",
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("instance_detail.html", context)
# ==================== Proxmox Server CRUD ====================
@router.get("/api/servers", response_model=List[ProxmoxServerResponse])

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from loguru import logger
@@ -16,12 +15,10 @@ from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerRe
from ...proxmox_client import ProxmoxClient, get_proxmox_resources
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
from ...logging_service import LoggingService
from ...template_helpers import add_i18n_context
from ...ipam_service import IPAMService
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
get_next_vmid, archive_and_delete_snapshots,
save_vm_instance, get_vm_instance, soft_delete_vm_instance,
templates)
save_vm_instance, get_vm_instance, soft_delete_vm_instance)
router = APIRouter()

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from loguru import logger
@@ -16,14 +15,12 @@ from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerRe
from ...proxmox_client import ProxmoxClient, get_proxmox_resources
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
from ...logging_service import LoggingService
from ...template_helpers import add_i18n_context
from ...ipam_service import IPAMService
from ...models import TaskQueue
from ...services.task_queue_service import TaskQueueService, process_task_queue
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
get_next_vmid, archive_and_delete_snapshots,
save_vm_instance, get_vm_instance, soft_delete_vm_instance,
templates)
save_vm_instance, get_vm_instance, soft_delete_vm_instance)
router = APIRouter()

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from loguru import logger
@@ -16,12 +15,10 @@ from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerRe
from ...proxmox_client import ProxmoxClient, get_proxmox_resources
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
from ...logging_service import LoggingService
from ...template_helpers import add_i18n_context
from ...ipam_service import IPAMService
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
get_next_vmid, archive_and_delete_snapshots,
save_vm_instance, get_vm_instance, soft_delete_vm_instance,
templates)
save_vm_instance, get_vm_instance, soft_delete_vm_instance)
router = APIRouter()