Админка: страницы Тикеты + Мониторинг, роль support в навигации

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 23:20:59 +07:00
parent 91e214dad2
commit 583682f330
6 changed files with 443 additions and 8 deletions
+66
View File
@@ -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<string | null | undefined>(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 (
<div className="p-6 sm:p-10 max-w-4xl mx-auto">
<p className="text-center py-20 text-muted-foreground text-sm uppercase tracking-widest">
Grafana ещё не настроена (GRAFANA_PUBLIC_URL не задан на сервере).
</p>
</div>
);
}
return (
<div className="p-6 sm:p-10 max-w-6xl mx-auto space-y-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<h1 className="text-xl font-black tracking-widest text-primary uppercase">
Мониторинг
</h1>
<div className="flex gap-2 flex-wrap">
{DASHBOARDS.map((d) => (
<button
key={d.uid}
onClick={() => setActive(d.uid)}
className={`px-3 py-1.5 rounded-lg text-xs font-bold uppercase tracking-widest transition-colors ${
active === d.uid
? "bg-primary/10 text-primary border border-primary/30"
: "bg-white/5 text-muted-foreground hover:bg-white/10"
}`}
>
{d.title}
</button>
))}
</div>
</div>
{/* Ключ на iframe меняет DOM-узел при смене дашборда — проще, чем
городить логику перезагрузки src внутри одного и того же iframe. */}
<iframe
key={active}
title={`Grafana: ${active}`}
src={`${grafanaUrl}/d/${active}/?kiosk&theme=dark`}
className="w-full rounded-2xl border border-white/10"
style={{ height: "80vh" }}
/>
</div>
);
}