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,46 +1,27 @@
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func, distinct
from datetime import datetime, timedelta
from ..db import get_db
from ..models import ProxmoxServer, AuditLog, User
from ..template_helpers import add_i18n_context
from ..auth import PermissionChecker
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False)
def dashboard(request: Request, db: Session = Depends(get_db)):
@router.get("/api/dashboard/stats")
def dashboard_stats(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("dashboard.view"))
):
"""Dashboard statistics API"""
from .workspaces import get_workspace_server_ids
# Try to get current user for workspace filtering (best-effort)
try:
token = request.cookies.get("access_token") or (
request.headers.get("Authorization", "").removeprefix("Bearer ").strip() or None
)
current_user_obj = None
if token:
from ..auth import decode_access_token
from ..models import User as UserModel
try:
payload = decode_access_token(token)
username = payload.get("sub")
if username:
current_user_obj = db.query(UserModel).filter(
UserModel.username == username, UserModel.is_active == True
).first()
except Exception:
pass
server_ids = get_workspace_server_ids(request, db, current_user)
server_ids = get_workspace_server_ids(request, db, current_user_obj) if current_user_obj else None
except Exception:
server_ids = None
from sqlalchemy import func, distinct
base_q = db.query(ProxmoxServer)
if server_ids is not None:
@@ -50,22 +31,14 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
online_servers = base_q.filter(ProxmoxServer.is_online == True).count()
offline_servers = base_q.filter(ProxmoxServer.is_online == False).count()
# Count distinct clusters within the filtered servers
cluster_q = db.query(func.count(distinct(ProxmoxServer.cluster_name))).filter(
ProxmoxServer.cluster_name.isnot(None),
ProxmoxServer.cluster_name != ''
)
standalone_q = db.query(ProxmoxServer).filter(
(ProxmoxServer.cluster_name == None) | (ProxmoxServer.cluster_name == '')
)
if server_ids is not None:
cluster_q = cluster_q.filter(ProxmoxServer.id.in_(server_ids))
standalone_q = standalone_q.filter(ProxmoxServer.id.in_(server_ids))
named_clusters = cluster_q.scalar() or 0
standalone_count = standalone_q.count()
total_clusters = named_clusters
total_clusters = cluster_q.scalar() or 0
# Alerts in last 24 hours
since_24h = datetime.utcnow() - timedelta(hours=24)
total_alerts = db.query(AuditLog).filter(
AuditLog.level.in_(['error', 'critical']),
@@ -76,7 +49,6 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
AuditLog.created_at >= since_24h
).count()
# Recent audit events (last 5)
recent_audit = db.query(AuditLog).order_by(
AuditLog.created_at.desc()
).limit(5).all()
@@ -85,29 +57,29 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
if server_ids is not None:
recent_servers_q = recent_servers_q.filter(ProxmoxServer.id.in_(server_ids))
recent_servers = recent_servers_q.order_by(ProxmoxServer.id.desc()).limit(6).all()
stats = {
"total_servers": total_servers,
"online_servers": online_servers,
"offline_servers": offline_servers,
"total_clusters": total_clusters,
"total_alerts": total_alerts,
"critical_alerts": critical_alerts,
return {
"stats": {
"total_servers": total_servers,
"online_servers": online_servers,
"offline_servers": offline_servers,
"total_clusters": total_clusters,
"total_alerts": total_alerts,
"critical_alerts": critical_alerts,
},
"recent_servers": [
{"id": s.id, "name": s.name, "ip_address": s.ip_address, "is_online": s.is_online}
for s in recent_servers
],
"recent_audit": [
{
"id": a.id, "level": a.level, "category": a.category,
"action": a.action, "message": a.message, "username": a.username,
"created_at": a.created_at.isoformat() if a.created_at else None,
}
for a in recent_audit
],
}
lang = request.cookies.get("language", "en")
from ..i18n import t
context = {
"request": request,
"stats": stats,
"recent_servers": recent_servers,
"recent_audit": recent_audit,
"page_title": t('nav_dashboard', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("dashboard.html", context)
@router.get("/api/dashboard/alerts")
@@ -139,16 +111,3 @@ def get_dashboard_alerts(
],
"total": len(alerts)
})
@router.get("/containers", response_class=HTMLResponse, include_in_schema=False)
def containers(request: Request):
lang = request.cookies.get("language", "en")
from ..i18n import t
context = {
"request": request,
"page_title": t('nav_docker', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("containers.html", context)