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,7 +1,6 @@
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, status, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from loguru import logger
@@ -19,23 +18,9 @@ from ..auth import (
)
from ..config import settings, utcnow
from ..logging_service import LoggingService
from ..template_helpers import add_i18n_context
from ..services.security_service import SecurityService
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@router.get("/login", response_class=HTMLResponse, include_in_schema=False)
def login_page(request: Request):
"""Login page"""
context = {
"request": request,
"panel_name": settings.PANEL_NAME,
"version": settings.VERSION,
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("login.html", context)
@router.post("/api/auth/login", response_model=Token)

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)

View File

@@ -8,8 +8,6 @@ import ipaddress as ipaddress_module
from typing import List, Optional
from pydantic import BaseModel
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from ..db import get_db
@@ -23,84 +21,9 @@ from ..schemas import (
)
from ..ipam_service import IPAMService
from ..auth import get_current_user, PermissionChecker
from ..template_helpers import add_i18n_context
logger = logging.getLogger(__name__)
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ==================== HTML Pages ====================
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
def ipam_dashboard(request: Request):
"""IPAM Dashboard page"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"page_title": t('nav_ipam', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("ipam_dashboard.html", context)
@router.get("/networks", response_class=HTMLResponse, include_in_schema=False)
def ipam_networks_page(request: Request):
"""IPAM Networks management page"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"page_title": t('nav_ipam', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("ipam_networks.html", context)
@router.get("/network/{network_id}", response_class=HTMLResponse, include_in_schema=False)
def ipam_network_detail(request: Request, network_id: int):
"""IPAM Network detail page with IP grid"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"network_id": network_id,
"page_title": t('nav_ipam', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("ipam_network_detail.html", context)
@router.get("/allocations", response_class=HTMLResponse, include_in_schema=False)
def ipam_allocations_page(request: Request):
"""IPAM Allocations page"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"page_title": t('nav_ipam', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("ipam_allocations.html", context)
@router.get("/history", response_class=HTMLResponse, include_in_schema=False)
def ipam_history_page(request: Request):
"""IPAM History page"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"page_title": t('nav_ipam', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("ipam_history.html", context)
# ==================== Network API ====================

View File

@@ -6,8 +6,6 @@ View and manage audit logs
from datetime import datetime, timedelta
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from pydantic import BaseModel
@@ -15,11 +13,9 @@ from ..db import get_db
from ..auth import get_current_user, PermissionChecker
from ..models import User, AuditLog
from ..logging_service import LoggingService
from ..template_helpers import add_i18n_context
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ==================== Schemas ====================
@@ -78,22 +74,6 @@ class LogStatsResponse(BaseModel):
recent_errors: List[dict]
# ==================== HTML Endpoints ====================
@router.get("/", response_class=HTMLResponse)
async def logs_page(request: Request):
"""Render logs page"""
from ..i18n import t
lang = request.cookies.get("language", "ru")
context = {
"request": request,
"title": t("nav_logs", lang),
"page_title": f"📋 {t('system_logs', lang)}"
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("logs.html", context)
# ==================== API Endpoints ====================
@router.get("/api/logs", response_model=LogsListResponse)

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()

View File

@@ -5,8 +5,7 @@ User account settings and panel configuration
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, status, Request, Query
from fastapi.responses import HTMLResponse, Response
from fastapi.templating import Jinja2Templates
from fastapi.responses import Response
from sqlalchemy.orm import Session
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
@@ -15,7 +14,6 @@ from ..db import get_db
from ..models import User, PanelSettings
from ..auth import get_current_user, get_current_user_optional, get_password_hash, verify_password, PermissionChecker
from ..logging_service import LoggingService
from ..template_helpers import add_i18n_context
from ..i18n import I18nService, t
from ..services.logo_service import LogoService
from ..services.update_service import (
@@ -33,7 +31,6 @@ def get_local_time() -> str:
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ==================== Helper Functions ====================
@@ -103,29 +100,6 @@ async def get_protected_logo():
)
# ==================== HTML Pages ====================
@router.get("/", response_class=HTMLResponse)
async def settings_page(
request: Request,
db: Session = Depends(get_db),
current_user: Optional[User] = Depends(get_current_user_optional)
):
"""Render settings page"""
from ..template_helpers import get_language
lang = get_language(request)
context = {
"request": request,
"title": t("nav_settings", lang),
"page_title": f"⚙️ {t('nav_settings', lang)}",
"current_user": current_user
}
# Add i18n context
context = add_i18n_context(request, context)
return templates.TemplateResponse("settings.html", context)
# ==================== User Account Settings ====================
@router.get("/api/profile")

View File

@@ -4,8 +4,7 @@ Allows creating VM templates, groups, and deploying VMs from templates
"""
from fastapi import APIRouter, Depends, Request, HTTPException, status
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
from typing import List
@@ -22,33 +21,8 @@ from ..api.proxmox import get_next_vmid, save_vm_instance, get_vm_instance
from ..auth import get_current_user, PermissionChecker
from ..ipam_service import IPAMService
from ..logging_service import LoggingService
from ..template_helpers import add_i18n_context
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ==================== HTML Pages ====================
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
def templates_page(request: Request, db: Session = Depends(get_db)):
"""Страница управления OS шаблонами"""
from ..i18n import t
lang = request.cookies.get("language", "en")
proxmox_servers = db.query(ProxmoxServer).all()
template_groups = db.query(OSTemplateGroup).order_by(OSTemplateGroup.sort_order).all()
os_templates = db.query(OSTemplate).order_by(OSTemplate.sort_order).all()
context = {
"request": request,
"proxmox_servers": proxmox_servers,
"template_groups": template_groups,
"os_templates": os_templates,
"page_title": t('nav_templates', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("os_templates.html", context)
# ==================== Template Groups CRUD ====================

View File

@@ -5,8 +5,6 @@ Users and Roles Management API
from datetime import datetime, timedelta
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field, EmailStr
from loguru import logger
@@ -23,27 +21,8 @@ from ..auth import (
from ..config import utcnow
from ..services.security_service import SecurityService
from ..logging_service import LoggingService
from ..template_helpers import add_i18n_context
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ==================== HTML Pages ====================
@router.get("/users", response_class=HTMLResponse, include_in_schema=False)
async def users_page(request: Request, db: Session = Depends(get_db)):
"""Users management page (admin only) - auth checked on frontend"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"page_title": t('nav_users', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("users.html", context)
# ==================== Schemas ====================
@@ -136,24 +115,6 @@ class SecuritySettingsUpdate(BaseModel):
password_require_special: Optional[bool] = None
# ==================== HTML Pages ====================
@router.get("/", response_class=HTMLResponse)
async def users_page(
request: Request,
):
"""Render users management page"""
from ..i18n import t
lang = request.cookies.get("language", "en")
context = {
"request": request,
"page_title": t('nav_users', lang),
}
context = add_i18n_context(request, context)
return templates.TemplateResponse("users.html", context)
# ==================== Role API ====================
@router.get("/api/roles", response_model=List[RoleResponse])

View File

@@ -11,8 +11,6 @@ All authenticated: list (filtered to their workspaces), get detail.
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from loguru import logger
@@ -20,11 +18,9 @@ from ..db import get_db
from ..models import Workspace, WorkspaceServer, WorkspaceUser, ProxmoxServer, User
from ..schemas import WorkspaceCreate, WorkspaceUpdate, WorkspaceResponse, WorkspaceDetail
from ..auth import get_current_user, PermissionChecker
from ..template_helpers import add_i18n_context
from ..logging_service import LoggingService
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
# ---------------------------------------------------------------------------
@@ -150,24 +146,6 @@ def _build_detail(ws: Workspace) -> WorkspaceDetail:
)
# ===========================================================================
# HTML Page
# ===========================================================================
@router.get("/workspaces", response_class=HTMLResponse, include_in_schema=False)
def workspaces_page(
request: Request,
):
"""Admin workspace management page"""
context = {
"request": request,
"page_title": "Workspaces",
}
from ..template_helpers import add_i18n_context
context = add_i18n_context(request, context)
return templates.TemplateResponse("workspaces.html", context)
# ===========================================================================
# REST API
# ===========================================================================