Replace servers nav with 3x-ui clients page showing enabled inbounds

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-25 22:39:07 +03:00
co-authored by Cursor
parent 0b3103bd78
commit 170927b8f8
4 changed files with 102 additions and 168 deletions
+38 -84
View File
@@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import AdminUser, Protocol, VpnClient, VpnServer, XuiPanel
from app.models import AdminUser, Protocol, VpnClient, VpnServer, XuiPanel, XuiPanelInbound, XuiSiteClient
from app.services import vpn as vpn_service
from app.services import xui_panels as xui_service
@@ -35,47 +35,41 @@ async def dashboard(request: Request, db: AsyncSession = Depends(get_db), admin=
if _is_redirect(admin):
return admin
servers = (await db.execute(select(VpnServer).order_by(VpnServer.id))).scalars().all()
clients_total = await db.scalar(select(func.count()).select_from(VpnClient)) or 0
clients_active = await db.scalar(
select(func.count()).select_from(VpnClient).where(VpnClient.is_enabled.is_(True))
xui_panels = await xui_service.list_panels(db)
xui_clients_total = await db.scalar(select(func.count()).select_from(XuiSiteClient)) or 0
user_inbounds_total = await db.scalar(
select(func.count())
.select_from(XuiPanelInbound)
.where(XuiPanelInbound.is_available_for_users.is_(True))
) or 0
user_inbounds_by_panel: dict[int, list] = {}
for panel in xui_panels:
rows = await xui_service.list_user_inbounds(db, panel.id)
user_inbounds_by_panel[panel.id] = [
{"inbound_id": r.inbound_id, "protocol": r.protocol, "remark": r.remark, "port": r.port}
for r in rows
]
return request.app.state.templates.TemplateResponse(
"admin/dashboard.html",
{
"request": request,
"admin": admin,
"servers": servers,
"clients_total": clients_total,
"clients_active": clients_active,
"xui_panels": xui_panels,
"xui_clients_total": xui_clients_total,
"user_inbounds_total": user_inbounds_total,
"user_inbounds_by_panel": user_inbounds_by_panel,
"app_name": request.app.state.settings.app_name,
},
)
@router.get("/servers", response_class=HTMLResponse)
async def servers_page(request: Request, db: AsyncSession = Depends(get_db), admin=Depends(require_admin)):
async def servers_page(admin=Depends(require_admin)):
"""WG/AWG servers UI временно скрыт — редирект на клиентов 3x-ui."""
if _is_redirect(admin):
return admin
servers = (
await db.execute(
select(VpnServer).options(selectinload(VpnServer.clients)).order_by(VpnServer.id)
)
).scalars().all()
probes = await vpn_service.probe_servers(list(servers))
return request.app.state.templates.TemplateResponse(
"admin/servers.html",
{
"request": request,
"admin": admin,
"servers": servers,
"probes": probes,
"protocols": Protocol,
"app_name": request.app.state.settings.app_name,
"flash": request.query_params.get("flash"),
},
)
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/servers")
@@ -230,83 +224,43 @@ async def update_server(
@router.get("/clients", response_class=HTMLResponse)
async def clients_page(
request: Request,
protocol: str | None = None,
source: str | None = None,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
tab = (source or protocol or "wg").lower()
if tab in ("wireguard", "awg2"):
tab = "wg"
if tab not in ("wg", "xui"):
tab = "wg"
query = select(VpnClient).options(selectinload(VpnClient.server)).order_by(VpnClient.id.desc())
if protocol in ("wireguard", "awg2"):
query = query.join(VpnServer).where(VpnServer.protocol == protocol)
tab = "wg"
clients = (await db.execute(query)).scalars().all()
servers = (await db.execute(select(VpnServer).order_by(VpnServer.id))).scalars().all()
xui_panels = await xui_service.list_panels(db)
xui_clients = await xui_service.list_all_site_clients(db) if tab == "xui" else []
xui_clients = await xui_service.list_all_site_clients(db)
user_inbounds_by_panel: dict[int, list] = {}
if tab == "xui":
for panel in xui_panels:
rows = await xui_service.list_user_inbounds(db, panel.id)
user_inbounds_by_panel[panel.id] = [
{
"inbound_id": r.inbound_id,
"protocol": r.protocol,
"remark": r.remark,
"port": r.port,
}
for r in rows
]
for panel in xui_panels:
rows = await xui_service.list_user_inbounds(db, panel.id)
user_inbounds_by_panel[panel.id] = [
{
"inbound_id": r.inbound_id,
"protocol": r.protocol,
"remark": r.remark,
"port": r.port,
}
for r in rows
]
panels_with_inbounds = [p for p in xui_panels if user_inbounds_by_panel.get(p.id)]
return request.app.state.templates.TemplateResponse(
"admin/clients.html",
{
"request": request,
"admin": admin,
"clients": clients,
"servers": servers,
"xui_panels": xui_panels,
"xui_clients": xui_clients,
"user_inbounds_by_panel": user_inbounds_by_panel,
"tab": tab,
"filter_protocol": protocol,
"protocols": Protocol,
"panels_with_inbounds": panels_with_inbounds,
"app_name": request.app.state.settings.app_name,
"flash": request.query_params.get("flash"),
},
)
@router.post("/clients")
async def create_client(
request: Request,
name: str = Form(...),
server_id: int = Form(...),
notes: str = Form(""),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
await vpn_service.create_client(db, server_id=server_id, name=name, notes=notes or None)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/clients?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse("/admin/clients?flash=created", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/clients/xui")
async def create_xui_client_from_clients(
request: Request,
@@ -328,7 +282,7 @@ async def create_xui_client_from_clients(
panel = await db.get(XuiPanel, panel_id)
if not panel:
return RedirectResponse(
"/admin/clients?source=xui&flash=error:Панель не найдена",
"/admin/clients?flash=error:Панель не найдена",
status_code=status.HTTP_303_SEE_OTHER,
)
try:
@@ -348,11 +302,11 @@ async def create_xui_client_from_clients(
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/clients?source=xui&flash=error:{exc}",
f"/admin/clients?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse(
"/admin/clients?source=xui&flash=created",
"/admin/clients?flash=created",
status_code=status.HTTP_303_SEE_OTHER,
)