feat: v1.1.3 — dashboard alerts panel, sparkline charts, log sanitisation, CSS vars

This commit is contained in:
markmorado
2026-03-10 14:10:00 +01:00
parent a0a502f82a
commit 3947dae7b9
16 changed files with 638 additions and 267 deletions

View File

@@ -1,13 +1,14 @@
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
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
from ..models import ProxmoxServer, AuditLog, User
from ..template_helpers import add_i18n_context
from ..rbac.decorators import PermissionChecker
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@@ -72,6 +73,37 @@ def dashboard(request: Request, db: Session = Depends(get_db)):
return templates.TemplateResponse("dashboard.html", context)
@router.get("/api/dashboard/alerts")
def get_dashboard_alerts(
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("logs.view"))
):
"""Вернуть события error/critical за последние 24 ч для дашборда"""
since_24h = datetime.utcnow() - timedelta(hours=24)
alerts = db.query(AuditLog).filter(
AuditLog.level.in_(['error', 'critical']),
AuditLog.created_at >= since_24h
).order_by(AuditLog.created_at.desc()).limit(50).all()
return JSONResponse(content={
"alerts": [
{
"id": a.id,
"level": a.level,
"category": a.category,
"action": a.action,
"message": a.message,
"username": a.username,
"resource_name": a.resource_name,
"server_name": a.server_name,
"created_at": a.created_at.isoformat() if a.created_at else None,
}
for a in alerts
],
"total": len(alerts)
})
@router.get("/containers", response_class=HTMLResponse, include_in_schema=False)
def containers(request: Request):
lang = request.cookies.get("language", "en")

View File

@@ -113,7 +113,7 @@ def create_proxmox_server(
db.commit()
db.refresh(server)
logger.info(f"User {current_user.username} added Proxmox server: {server.name} ({server.ip_address})")
logger.info(f"User {current_user.username} added Proxmox server: {server.name}")
return server
@@ -187,7 +187,7 @@ async def auto_setup_proxmox_server(
detail="Не удалось получить auth ticket от Proxmox"
)
logger.info(f"Successfully authenticated to Proxmox {ip_address}")
logger.info("Successfully authenticated to Proxmox")
except http_requests.exceptions.RequestException as e:
logger.error(f"Connection error to Proxmox: {e}")
@@ -227,7 +227,7 @@ async def auto_setup_proxmox_server(
else:
token_data = token_response.json().get("data", {})
token_value = token_data.get("value")
logger.info(f"Created API token: {token_name}")
logger.info("Created API token successfully")
except Exception as e:
logger.warning(f"Error creating token: {e}, will use password auth")
@@ -346,7 +346,7 @@ async def auto_setup_proxmox_server(
db.commit()
db.refresh(server)
logger.info(f"User {current_user.username} auto-setup Proxmox server: {server.name} ({server.ip_address})")
logger.info(f"User {current_user.username} auto-setup Proxmox server: {server.name}")
return {
"cluster": False,
@@ -624,7 +624,7 @@ def get_all_resources(
for server in proxmox_servers:
try:
logger.info(f"Getting resources from server {server.name} ({server.ip_address}), use_password={server.use_password}")
logger.info(f"Getting resources from server {server.name}, use_password={server.use_password}")
# Создаём клиента для каждого сервера независимо
if server.use_password:
resources = get_proxmox_resources(
@@ -634,7 +634,7 @@ def get_all_resources(
verify_ssl=server.verify_ssl
)
else:
logger.info(f"Using API token for {server.name}: user={server.api_user}, token_name={server.api_token_name}")
logger.info(f"Using API token for {server.name}: user={server.api_user}")
resources = get_proxmox_resources(
host=server.ip_address,
user=server.api_user,

View File

@@ -1206,7 +1206,7 @@ def get_server_nodes(
except Exception:
pass
logger.info(f"Nodes for server {server_id} ({server.name}), cluster_id={cluster_id}: {nodes}")
logger.info(f"Nodes for server {server_id} ({server.name}), cluster_id={cluster_id}: {len(nodes)} node(s)")
return JSONResponse(content={"nodes": nodes, "cluster_id": cluster_id})
except Exception as e:
logger.error(f"Error getting nodes from {server.name}: {e}")
@@ -1611,7 +1611,7 @@ async def vnc_websocket_proxy(
encoded_ticket = urllib.parse.quote(vncticket, safe='')
proxmox_ws_url = f"wss://{server.ip_address}:8006/api2/json/nodes/{node}/{proxmox_vmtype}/{vmid}/vncwebsocket?port={port}&vncticket={encoded_ticket}"
logger.info(f"Connecting to Proxmox VNC: {server.ip_address}:8006 for {proxmox_vmtype}/{vmid}")
logger.info(f"Connecting to Proxmox VNC: {server.name} for {proxmox_vmtype}/{vmid}")
# SSL контекст для самоподписанных сертификатов
ssl_context = ssl.create_default_context()