update
This commit is contained in:
parent
61b50df864
commit
5ef8c35ca8
|
|
@ -16,6 +16,8 @@ import {
|
|||
AdminBlockchainPage,
|
||||
AdminNodesPage,
|
||||
AdminStatusPage,
|
||||
AdminNetworkPage,
|
||||
AdminNetworkSettingsPage,
|
||||
} from "~/pages/admin/sections";
|
||||
import { ProtectedLayout } from "./protected-layout";
|
||||
|
||||
|
|
@ -59,6 +61,8 @@ const router = createBrowserRouter([
|
|||
{ path: "blockchain", element: <AdminBlockchainPage /> },
|
||||
{ path: "nodes", element: <AdminNodesPage /> },
|
||||
{ path: "status", element: <AdminStatusPage /> },
|
||||
{ path: "network", element: <AdminNetworkPage /> },
|
||||
{ path: "network-settings", element: <AdminNetworkSettingsPage /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ export type AdminSectionId =
|
|||
| "system"
|
||||
| "blockchain"
|
||||
| "nodes"
|
||||
| "status";
|
||||
| "status"
|
||||
| "network"
|
||||
| "network-settings";
|
||||
|
||||
export type AdminSection = {
|
||||
id: AdminSectionId;
|
||||
|
|
@ -33,6 +35,8 @@ export const ADMIN_SECTIONS: AdminSection[] = [
|
|||
{ id: "blockchain", label: "Блокчейн", description: "История задач и метрики блокчейн-интеграции", path: "blockchain" },
|
||||
{ id: "nodes", label: "Ноды", description: "Роли, версии и последнее появление узлов", path: "nodes" },
|
||||
{ id: "status", label: "Статус & лимиты", description: "IPFS, очереди и лимиты синхронизации", path: "status" },
|
||||
{ id: "network", label: "Состояние сети", description: "Мониторинг децентрализованного слоя и репликаций", path: "network" },
|
||||
{ id: "network-settings", label: "Сеть → Настройки", description: "Интервалы heartbeat/gossip и бэк-офф", path: "network-settings" },
|
||||
];
|
||||
|
||||
export const DEFAULT_ADMIN_SECTION = ADMIN_SECTIONS[0];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,329 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { request } from "~/shared/libs/request";
|
||||
|
||||
const ratioTone = (v: number) => {
|
||||
if (v >= 0.9) return "text-emerald-400";
|
||||
if (v >= 0.6) return "text-yellow-400";
|
||||
return "text-red-400";
|
||||
};
|
||||
|
||||
export const AdminNetworkPage: React.FC = () => {
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [reachabilityFilter, setReachabilityFilter] = useState<"all" | "healthy" | "islands">("all");
|
||||
const [sortBy, setSortBy] = useState<"leases" | "leaderships" | "reachability">("leases");
|
||||
const [roleFilter, setRoleFilter] = useState<"all" | "trusted" | "read-only" | "deny">("all");
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(25);
|
||||
|
||||
const { data, isLoading, error, refetch } = useQuery(
|
||||
["admin", "network", page, pageSize],
|
||||
async () => {
|
||||
const { data } = await request.get("/admin.network", { params: { page, page_size: pageSize } });
|
||||
return data as any;
|
||||
},
|
||||
{ keepPreviousData: true },
|
||||
);
|
||||
|
||||
const members = data?.members ?? [];
|
||||
const replication = data?.per_node_replication ?? {};
|
||||
const summary = data?.summary;
|
||||
const receipts = data?.receipts ?? [];
|
||||
|
||||
const [selectedNode, setSelectedNode] = useState<any | null>(null);
|
||||
|
||||
const total = data?.paging?.total ?? (data?.members?.length ?? 0);
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
||||
|
||||
const columns = [
|
||||
{ key: "node_id", label: "NodeID" },
|
||||
{ key: "public_host", label: "Публичный адрес" },
|
||||
{ key: "version", label: "Версия" },
|
||||
{ key: "role", label: "Роль" },
|
||||
{ key: "ip", label: "IP" },
|
||||
{ key: "asn", label: "ASN" },
|
||||
{ key: "reachability_ratio", label: "Достижимость" },
|
||||
{ key: "leases", label: "Реплики" },
|
||||
{ key: "leaderships", label: "Лидерства" },
|
||||
{ key: "receipts", label: "Квитанции" },
|
||||
];
|
||||
|
||||
const rows = useMemo(() => {
|
||||
let list = members.map((m: any) => {
|
||||
const p = replication[m.node_id] ?? { leases_held: 0, leaderships: 0, sample_contents: [] };
|
||||
return {
|
||||
...m,
|
||||
leases: p.leases_held,
|
||||
leaderships: p.leaderships,
|
||||
_samples: p.sample_contents,
|
||||
};
|
||||
});
|
||||
// filter by text
|
||||
if (filterText.trim()) {
|
||||
const t = filterText.trim().toLowerCase();
|
||||
list = list.filter((r: any) =>
|
||||
(r.public_host ?? "").toLowerCase().includes(t) ||
|
||||
(r.ip ?? "").toLowerCase().includes(t) ||
|
||||
(r.node_id ?? "").toLowerCase().includes(t)
|
||||
);
|
||||
}
|
||||
// role filter
|
||||
if (roleFilter !== "all") {
|
||||
list = list.filter((r: any) => (r.role ?? "read-only") === roleFilter);
|
||||
}
|
||||
// reachability filter
|
||||
if (reachabilityFilter === "healthy") {
|
||||
list = list.filter((r: any) => r.reachability_ratio >= 0.6);
|
||||
} else if (reachabilityFilter === "islands") {
|
||||
list = list.filter((r: any) => r.reachability_ratio < 0.6);
|
||||
}
|
||||
// sorting
|
||||
list.sort((a: any, b: any) => {
|
||||
if (sortBy === "leases") return b.leases - a.leases;
|
||||
if (sortBy === "leaderships") return b.leaderships - a.leaderships;
|
||||
return b.reachability_ratio - a.reachability_ratio;
|
||||
});
|
||||
// reset page if filters reduce list
|
||||
return list;
|
||||
}, [members, replication, filterText, reachabilityFilter, sortBy, roleFilter]);
|
||||
const paged = rows; // сервер уже порезал список
|
||||
|
||||
if (isLoading) return <div className="p-4">Загрузка сети…</div>;
|
||||
if (error) return <div className="p-4 text-red-400">Ошибка загрузки: {String(error)}</div>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-6 gap-4">
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Оценка размера сети</div>
|
||||
<div className="text-2xl font-semibold">{summary?.n_estimate ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Оценка trusted‑сети</div>
|
||||
<div className="text-2xl font-semibold">{summary?.n_estimate_trusted ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Активные (все)</div>
|
||||
<div className="text-2xl font-semibold">{summary?.active ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Активные (trusted)</div>
|
||||
<div className="text-2xl font-semibold">{summary?.active_trusted ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Острова</div>
|
||||
<div className="text-2xl font-semibold text-yellow-400">{summary?.islands ?? 0}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Конфликты репликаций</div>
|
||||
<div className="text-sm">Недобор: <span className="text-red-400">{summary?.replication_conflicts.under ?? 0}</span></div>
|
||||
<div className="text-sm">Перебор: <span className="text-yellow-400">{summary?.replication_conflicts.over ?? 0}</span></div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-slate-800/50 p-3">
|
||||
<div className="text-xs text-slate-400">Gossip/Backoff</div>
|
||||
<div className="text-sm">Интервал: {summary?.config?.gossip_interval_sec ?? '-'} c</div>
|
||||
<div className="text-sm">База: {summary?.config?.gossip_backoff_base_sec ?? '-'} c</div>
|
||||
<div className="text-sm">Потолок: {summary?.config?.gossip_backoff_cap_sec ?? '-' } c</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-slate-800/50 overflow-hidden">
|
||||
<div className="px-3 py-2 text-sm text-slate-300 border-b border-slate-700 flex justify-between items-center">
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<div>Узлы сети</div>
|
||||
<input
|
||||
className="bg-slate-900/50 text-sm px-2 py-1 rounded border border-slate-700 outline-none"
|
||||
placeholder="Фильтр: host/IP/NodeID"
|
||||
value={filterText}
|
||||
onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="bg-slate-900/50 text-sm px-2 py-1 rounded border border-slate-700"
|
||||
value={roleFilter}
|
||||
onChange={(e) => setRoleFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">Любая роль</option>
|
||||
<option value="trusted">Только trusted</option>
|
||||
<option value="read-only">Только read-only</option>
|
||||
<option value="deny">Только deny</option>
|
||||
</select>
|
||||
<select
|
||||
className="bg-slate-900/50 text-sm px-2 py-1 rounded border border-slate-700"
|
||||
value={reachabilityFilter}
|
||||
onChange={(e) => setReachabilityFilter(e.target.value as any)}
|
||||
>
|
||||
<option value="all">Все</option>
|
||||
<option value="healthy">Только здоровые</option>
|
||||
<option value="islands">Только острова</option>
|
||||
</select>
|
||||
<select
|
||||
className="bg-slate-900/50 text-sm px-2 py-1 rounded border border-slate-700"
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
>
|
||||
<option value="leases">По репликам</option>
|
||||
<option value="leaderships">По лидерствам</option>
|
||||
<option value="reachability">По достижимости</option>
|
||||
</select>
|
||||
<select
|
||||
className="bg-slate-900/50 text-sm px-2 py-1 rounded border border-slate-700"
|
||||
value={pageSize}
|
||||
onChange={(e) => { setPage(1); setPageSize(parseInt(e.target.value)); }}
|
||||
>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onClick={() => refetch()} className="text-xs px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded">
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/40">
|
||||
{columns.map((c) => (
|
||||
<th key={c.key} className="px-3 py-2 text-left text-slate-400 font-medium whitespace-nowrap">{c.label}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paged.map((r: any) => (
|
||||
<tr key={r.node_id} className="hover:bg-slate-900/30 cursor-pointer" onClick={() => setSelectedNode(r)}>
|
||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{r.node_id.slice(0, 10)}…</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.public_host ?? '-'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.version ?? '-'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.role}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.ip ?? '-'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.asn ?? '-'}</td>
|
||||
<td className={`px-3 py-2 whitespace-nowrap ${ratioTone(r.reachability_ratio)}`}>{(r.reachability_ratio * 100).toFixed(0)}%</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.leases}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.leaderships}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.receipts_asn_unique ?? 0}/{r.receipts_total ?? 0}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Pagination controls */}
|
||||
<div className="px-3 py-2 border-t border-slate-700 flex items-center justify-between text-xs text-slate-400">
|
||||
<div>Стр. {page} из {totalPages} • Всего: {total}</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-2 py-1 bg-slate-700 rounded disabled:opacity-50" onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>Назад</button>
|
||||
<button className="px-2 py-1 bg-slate-700 rounded disabled:opacity-50" onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={page >= totalPages}>Вперёд</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reachability receipts table */}
|
||||
<div className="rounded-lg bg-slate-800/50 overflow-hidden">
|
||||
<div className="px-3 py-2 text-sm text-slate-300 border-b border-slate-700 flex justify-between items-center">
|
||||
<div>Квитанции достижимости</div>
|
||||
<div className="text-xs text-slate-400">{receipts.length} шт.</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-slate-900/40">
|
||||
<th className="px-3 py-2 text-left text-slate-400 font-medium whitespace-nowrap">Target</th>
|
||||
<th className="px-3 py-2 text-left text-slate-400 font-medium whitespace-nowrap">Issuer</th>
|
||||
<th className="px-3 py-2 text-left text-slate-400 font-medium whitespace-nowrap">ASN</th>
|
||||
<th className="px-3 py-2 text-left text-slate-400 font-medium whitespace-nowrap">Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{receipts.map((r: any, i: number) => (
|
||||
<tr key={`${r.issuer_id}:${r.target_id}:${i}`} className="hover:bg-slate-900/30">
|
||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{r.target_id.slice(0, 10)}…</td>
|
||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{r.issuer_id.slice(0, 10)}…</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">{r.asn ?? '-'}</td>
|
||||
<td className="px-3 py-2 whitespace-nowrap">
|
||||
{r.status === 'valid' && <span className="text-emerald-400">подпись ок</span>}
|
||||
{r.status === 'bad_signature' && <span className="text-red-400">ошибка подписи</span>}
|
||||
{r.status === 'unknown_issuer' && <span className="text-yellow-400">неизвестный эмитент</span>}
|
||||
{r.status === 'mismatch_node_id' && <span className="text-orange-400">node_id≠pubkey</span>}
|
||||
{r.status === 'unknown' && <span className="text-slate-400">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-500">
|
||||
Подсказка: недобор/перебор репликаций сигнализируют о проблемах с диверсификацией или подсчётом N_estimate.
|
||||
</div>
|
||||
|
||||
{selectedNode && (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div className="bg-slate-900 rounded-lg border border-slate-700 w-[min(92vw,720px)] max-h-[80vh] overflow-auto">
|
||||
<div className="px-4 py-3 border-b border-slate-700 flex items-center justify-between">
|
||||
<div className="text-sm text-slate-300">Детали узла</div>
|
||||
<button className="text-xs px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded" onClick={() => setSelectedNode(null)}>Закрыть</button>
|
||||
</div>
|
||||
<div className="p-4 text-sm space-y-2">
|
||||
<div><span className="text-slate-400">NodeID:</span> <span className="font-mono text-xs">{selectedNode.node_id}</span></div>
|
||||
<div><span className="text-slate-400">Публичный адрес:</span> {selectedNode.public_host ?? '—'}</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div><span className="text-slate-400">Версия:</span> {selectedNode.version ?? '—'}</div>
|
||||
<div><span className="text-slate-400">Роль:</span> {selectedNode.role}</div>
|
||||
<div><span className="text-slate-400">IP:</span> {selectedNode.ip ?? '—'}</div>
|
||||
<div><span className="text-slate-400">ASN:</span> {selectedNode.asn ?? '—'}</div>
|
||||
<div><span className="text-slate-400">Достижимость:</span> {(selectedNode.reachability_ratio * 100).toFixed(0)}%</div>
|
||||
<div><span className="text-slate-400">Квитанции:</span> {selectedNode.receipts_asn_unique ?? 0}/{selectedNode.receipts_total ?? 0}</div>
|
||||
<div><span className="text-slate-400">Реплики:</span> {selectedNode.leases}</div>
|
||||
<div><span className="text-slate-400">Лидерства:</span> {selectedNode.leaderships}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-400 mb-1">Примеры контента:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(selectedNode._samples ?? []).map((c: string) => (
|
||||
<span key={c} className="px-2 py-1 bg-slate-800 rounded text-xs font-mono">{c.slice(0,10)}…</span>
|
||||
))}
|
||||
{(!selectedNode._samples || selectedNode._samples.length === 0) && (
|
||||
<span className="text-slate-500">—</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-400 mb-1">Конфликты:</div>
|
||||
<div className="max-h-40 overflow-auto border border-slate-800 rounded">
|
||||
<table className="min-w-full text-xs">
|
||||
<thead className="bg-slate-900">
|
||||
<tr>
|
||||
<th className="px-2 py-1 text-left text-slate-400">Тип</th>
|
||||
<th className="px-2 py-1 text-left text-slate-400">Время</th>
|
||||
<th className="px-2 py-1 text-left text-slate-400">Content</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(selectedNode.conflict_samples ?? []).map((item: any, idx: number) => (
|
||||
<tr key={idx} className="odd:bg-slate-900/40">
|
||||
<td className="px-2 py-1 whitespace-nowrap">{item.type}</td>
|
||||
<td className="px-2 py-1 whitespace-nowrap">{item.ts ? new Date(item.ts * 1000).toLocaleString() : '—'}</td>
|
||||
<td className="px-2 py-1 font-mono whitespace-nowrap">{(item.content_id || '').slice(0, 10)}…</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!selectedNode.conflict_samples || selectedNode.conflict_samples.length === 0) && (
|
||||
<tr><td colSpan={3} className="px-2 py-2 text-slate-500 text-center">Нет данных</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{selectedNode.public_host && (
|
||||
<div className="pt-2">
|
||||
<a href={selectedNode.public_host} target="_blank" rel="noreferrer" className="text-emerald-400 hover:underline">Открыть узел</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminNetworkPage;
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { request } from "~/shared/libs/request";
|
||||
|
||||
type Config = {
|
||||
heartbeat_interval: number;
|
||||
lease_ttl: number;
|
||||
gossip_interval_sec: number;
|
||||
gossip_backoff_base_sec: number;
|
||||
gossip_backoff_cap_sec: number;
|
||||
};
|
||||
|
||||
export const AdminNetworkSettingsPage: React.FC = () => {
|
||||
const [cfg, setCfg] = useState<Config | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const load = async () => {
|
||||
try {
|
||||
const { data } = await request.get("/admin.network.config");
|
||||
setCfg(data.config);
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message || e));
|
||||
}
|
||||
};
|
||||
useEffect(() => { void load(); }, []);
|
||||
|
||||
const update = async () => {
|
||||
if (!cfg) return;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await request.post("/admin.network.config.set", cfg);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
setError(String(e?.message || e));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!cfg) return <div className="p-4">Загрузка настроек…</div>;
|
||||
|
||||
const Field: React.FC<{ label: string; value: number; onChange: (v:number)=>void; tip?: string }> = ({label, value, onChange, tip}) => (
|
||||
<label className="flex flex-col gap-1">
|
||||
<span className="text-xs text-slate-400">{label}</span>
|
||||
<input type="number" className="bg-slate-900/50 px-2 py-1 rounded border border-slate-700" value={value} onChange={(e)=>onChange(parseInt(e.target.value || '0'))} />
|
||||
{tip && <span className="text-[10px] text-slate-500">{tip}</span>}
|
||||
</label>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
<div className="text-lg font-semibold">Сеть → Настройки</div>
|
||||
{error && <div className="text-sm text-red-400">Ошибка: {error}</div>}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Field label="Интервал heartbeat (сек)" value={cfg.heartbeat_interval} onChange={(v)=>setCfg({...cfg, heartbeat_interval: v})} tip="Период обновления лизов" />
|
||||
<Field label="TTL лиза (сек)" value={cfg.lease_ttl} onChange={(v)=>setCfg({...cfg, lease_ttl: v})} tip="Время жизни лиза до истечения" />
|
||||
<Field label="Интервал gossip (сек)" value={cfg.gossip_interval_sec} onChange={(v)=>setCfg({...cfg, gossip_interval_sec: v})} tip="Период рассылки снимка DHT" />
|
||||
<Field label="Бэк-офф база (сек)" value={cfg.gossip_backoff_base_sec} onChange={(v)=>setCfg({...cfg, gossip_backoff_base_sec: v})} tip="Начальный бэк-офф для неуспешных пиров" />
|
||||
<Field label="Бэк-офф потолок (сек)" value={cfg.gossip_backoff_cap_sec} onChange={(v)=>setCfg({...cfg, gossip_backoff_cap_sec: v})} tip="Максимальный бэк-офф" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button className="px-3 py-2 bg-emerald-700 hover:bg-emerald-600 rounded disabled:opacity-50" onClick={()=>void update()} disabled={saving}>Сохранить</button>
|
||||
<button className="px-3 py-2 bg-slate-700 hover:bg-slate-600 rounded" onClick={()=>void load()}>Сбросить</button>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">Примечание: некоторые параметры читаются задачами-демонами при каждом цикле и применяются без перезапуска.</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminNetworkSettingsPage;
|
||||
|
||||
|
|
@ -9,3 +9,5 @@ export { AdminSystemPage } from "./System";
|
|||
export { AdminBlockchainPage } from "./Blockchain";
|
||||
export { AdminNodesPage } from "./Nodes";
|
||||
export { AdminStatusPage } from "./Status";
|
||||
export { AdminNetworkPage } from "./Network";
|
||||
export { AdminNetworkSettingsPage } from "./NetworkSettings";
|
||||
|
|
|
|||
|
|
@ -340,17 +340,17 @@ export const ViewContentPage = () => {
|
|||
/>
|
||||
)}
|
||||
{!haveLicense && (
|
||||
<div className="flex gap-4 pb-2">
|
||||
<div className="flex gap-4 pb-2 flex-nowrap overflow-hidden">
|
||||
<Button
|
||||
onClick={handleBuyContentTON}
|
||||
className={'mb-4 h-[48px] px-2 flex-1 min-w-[140px] w-auto'}
|
||||
className={'mb-4 h-[48px] px-2 flex-1 min-w-0 w-auto truncate'}
|
||||
label={`Купить за ${fromNanoTON(content?.data?.encrypted?.license?.resale?.price)} ТОН`}
|
||||
includeArrows={content?.data?.invoice ? false : true}
|
||||
/>
|
||||
{content?.data?.invoice && (
|
||||
<Button
|
||||
onClick={handleBuyContentStars}
|
||||
className={'mb-4 h-[48px] px-2 flex-1 min-w-[140px] w-auto'}
|
||||
className={'mb-4 h-[48px] px-2 flex-1 min-w-0 w-auto truncate'}
|
||||
label={`Купить за ${content?.data?.invoice?.amount} ⭐️`}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -576,6 +576,78 @@ export type AdminNodesResponse = {
|
|||
}>;
|
||||
};
|
||||
|
||||
// --------- Состояние сети (новый раздел) ---------
|
||||
|
||||
export type AdminNetworkMember = {
|
||||
node_id: string;
|
||||
public_key: string | null;
|
||||
public_host: string | null;
|
||||
version: string | null;
|
||||
role: string;
|
||||
ip: string | null;
|
||||
asn: number | null;
|
||||
ip_first_octet: number | null;
|
||||
reachability_ratio: number;
|
||||
last_update: number | null;
|
||||
accepts_inbound: boolean;
|
||||
is_bootstrap: boolean;
|
||||
receipts_total?: number;
|
||||
receipts_asn_unique?: number;
|
||||
};
|
||||
|
||||
export type AdminNetworkPerNodeReplication = Record<
|
||||
string,
|
||||
{
|
||||
leases_held: number;
|
||||
leaderships: number;
|
||||
sample_contents: string[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type AdminNetworkResponse = {
|
||||
summary: {
|
||||
n_estimate: number;
|
||||
n_estimate_trusted: number;
|
||||
active_trusted: number;
|
||||
members_total: number;
|
||||
active: number;
|
||||
islands: number;
|
||||
replication_conflicts: { under: number; over: number };
|
||||
config: {
|
||||
heartbeat_interval: number;
|
||||
lease_ttl: number;
|
||||
gossip_interval_sec: number;
|
||||
gossip_backoff_base_sec: number;
|
||||
gossip_backoff_cap_sec: number;
|
||||
};
|
||||
};
|
||||
members: AdminNetworkMember[];
|
||||
per_node_replication: AdminNetworkPerNodeReplication;
|
||||
receipts: Array<{
|
||||
target_id: string;
|
||||
issuer_id: string;
|
||||
asn: number | null;
|
||||
timestamp: number | null;
|
||||
status: 'valid' | 'bad_signature' | 'unknown_issuer' | 'mismatch_node_id' | 'unknown';
|
||||
}>;
|
||||
};
|
||||
|
||||
export const useAdminNetwork = (
|
||||
options?: QueryOptions<AdminNetworkResponse, ['admin', 'network']>,
|
||||
) => {
|
||||
return useQuery<AdminNetworkResponse, AxiosError, AdminNetworkResponse, ['admin', 'network']>(
|
||||
['admin', 'network'],
|
||||
async () => {
|
||||
const { data } = await request.get<AdminNetworkResponse>('/admin.network');
|
||||
return data;
|
||||
},
|
||||
{
|
||||
...defaultQueryOptions,
|
||||
...options,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export type AdminStatusResponse = {
|
||||
ipfs: {
|
||||
bitswap: Record<string, unknown>;
|
||||
|
|
|
|||
22
yarn.lock
22
yarn.lock
|
|
@ -225,10 +225,10 @@
|
|||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@esbuild/darwin-arm64@0.19.12":
|
||||
"@esbuild/linux-x64@0.19.12":
|
||||
version "0.19.12"
|
||||
resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz"
|
||||
integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==
|
||||
resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz"
|
||||
integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
|
||||
version "4.4.0"
|
||||
|
|
@ -381,10 +381,15 @@
|
|||
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.15.2.tgz"
|
||||
integrity sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.12.0":
|
||||
"@rollup/rollup-linux-x64-gnu@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz"
|
||||
integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz"
|
||||
integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.12.0":
|
||||
version "4.12.0"
|
||||
resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz"
|
||||
integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==
|
||||
|
||||
"@sentry-internal/browser-utils@9.1.0":
|
||||
version "9.1.0"
|
||||
|
|
@ -1655,11 +1660,6 @@ fs.realpath@^1.0.0:
|
|||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
|
|
|
|||
Loading…
Reference in New Issue