From 583682f3307e916fe1747a710587677ef3dc5d1c Mon Sep 17 00:00:00 2001 From: nineap Date: Thu, 9 Jul 2026 23:20:59 +0700 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0:=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D0=A2=D0=B8?= =?UTF-8?q?=D0=BA=D0=B5=D1=82=D1=8B=20+=20=D0=9C=D0=BE=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D0=BE=D1=80=D0=B8=D0=BD=D0=B3,=20=D1=80=D0=BE=D0=BB=D1=8C=20su?= =?UTF-8?q?pport=20=D0=B2=20=D0=BD=D0=B0=D0=B2=D0=B8=D0=B3=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TicketsPage.tsx — список open/closed тикетов + тред сообщений + ответ + закрытие (owner/moderator/support). ObservabilityPage.tsx — вкладки дашбордов Grafana через iframe за auth-гейтом Caddy, URL берётся из GET /api/v1/config (grafana_url), страница просто не показывает дашборды, пока GRAFANA_PUBLIC_URL не задан на сервере. StaffPage.tsx — форма создания support-аккаунта (только Owner). Co-Authored-By: Claude Sonnet 5 --- frontend/src/AdminLayout.tsx | 22 ++- frontend/src/App.tsx | 4 + frontend/src/ObservabilityPage.tsx | 66 +++++++++ frontend/src/StaffPage.tsx | 52 +++++++ frontend/src/TicketsPage.tsx | 220 +++++++++++++++++++++++++++++ frontend/src/api.ts | 87 +++++++++++- 6 files changed, 443 insertions(+), 8 deletions(-) create mode 100644 frontend/src/ObservabilityPage.tsx create mode 100644 frontend/src/TicketsPage.tsx diff --git a/frontend/src/AdminLayout.tsx b/frontend/src/AdminLayout.tsx index f732b60..21c381e 100644 --- a/frontend/src/AdminLayout.tsx +++ b/frontend/src/AdminLayout.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; -import { NavLink, Outlet, useNavigate } from "react-router-dom"; -import { Loader2, Server, Users, Wallet, DollarSign, LogOut } from "lucide-react"; +import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; +import { Loader2, Server, Users, Wallet, DollarSign, LogOut, LifeBuoy, Activity } from "lucide-react"; import { fetchMyRole, logout, type StaffRole } from "./api"; -const STAFF_ROLES = ["owner", "moderator", "partner"]; +const STAFF_ROLES = ["owner", "moderator", "partner", "support"]; export default function AdminLayout() { const navigate = useNavigate(); + const location = useLocation(); const [role, setRole] = useState(null); const [loading, setLoading] = useState(true); @@ -17,6 +18,11 @@ export default function AdminLayout() { navigate("/admin/login"); return; } + // Support не видит "Ноды" (индекс-роут /admin) — после логина сразу + // на единственный раздел, который ему доступен. + if (r.role === "support" && location.pathname === "/admin") { + navigate("/admin/tickets", { replace: true }); + } setRole(r); }) .catch(() => navigate("/admin/login")) @@ -66,6 +72,16 @@ export default function AdminLayout() { Выводы )} + {(role.role === "owner" || role.role === "moderator" || role.role === "support") && ( + + Тикеты + + )} + {(role.role === "owner" || role.role === "moderator") && ( + + Мониторинг + + )} {role.role === "partner" && ( Доходы diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c8e68c3..e0a4a5e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,8 @@ const AdminLayout = lazy(() => import("./AdminLayout")); const StaffPage = lazy(() => import("./StaffPage")); const WithdrawalsPage = lazy(() => import("./WithdrawalsPage")); const EarningsPage = lazy(() => import("./EarningsPage")); +const TicketsPage = lazy(() => import("./TicketsPage")); +const ObservabilityPage = lazy(() => import("./ObservabilityPage")); const DICT = { en: { loading: "Loading protocols..." }, @@ -94,6 +96,8 @@ export default function App() { } /> } /> } /> + } /> + } /> diff --git a/frontend/src/ObservabilityPage.tsx b/frontend/src/ObservabilityPage.tsx new file mode 100644 index 0000000..33425e7 --- /dev/null +++ b/frontend/src/ObservabilityPage.tsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from "react"; +import { getPublicConfig } from "./api"; + +const DASHBOARDS = [ + { uid: "netrunner-backend", title: "Backend" }, + { uid: "netrunner-landing", title: "Лендинг" }, + { uid: "netrunner-proxy-nodes", title: "Прокси-ноды (ядро)" }, +] as const; + +export default function ObservabilityPage() { + const [grafanaUrl, setGrafanaUrl] = useState(undefined); + const [active, setActive] = useState<(typeof DASHBOARDS)[number]["uid"]>(DASHBOARDS[0].uid); + + useEffect(() => { + getPublicConfig().then((cfg) => setGrafanaUrl(cfg.grafana_url)); + }, []); + + if (grafanaUrl === undefined) { + return null; + } + + if (!grafanaUrl) { + return ( +
+

+ Grafana ещё не настроена (GRAFANA_PUBLIC_URL не задан на сервере). +

+
+ ); + } + + return ( +
+
+

+ Мониторинг +

+
+ {DASHBOARDS.map((d) => ( + + ))} +
+
+ + {/* Ключ на iframe меняет DOM-узел при смене дашборда — проще, чем + городить логику перезагрузки src внутри одного и того же iframe. */} +