Загрузить файлы в «dashboard »
This commit is contained in:
@@ -0,0 +1,416 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
truenas: {
|
||||||
|
apps: [
|
||||||
|
{ name: "SearXNG", image: "searxng/searxng", version: "2026.4.29", ports: ["30053"], status: "up", uptime: "3 мин", icon: "🔍", category: "Утилиты" },
|
||||||
|
{ name: "Beszel Hub", image: "henrygd/beszel", version: "0.18.7", ports: ["30333"], status: "up", uptime: "5 мин", icon: "📊", category: "Мониторинг" },
|
||||||
|
{ name: "TeamSpeak", image: "teamspeak", version: "3.13.7", ports: ["10011","10022","9987/udp","30033"], status: "up", uptime: "5 мин", icon: "🎙️", category: "Коммуникации" },
|
||||||
|
{ name: "MariaDB", image: "mariadb", version: "11.8.6", ports: ["3306 (.31.100)"], status: "up", uptime: "5 мин", icon: "🗄️", category: "БД" },
|
||||||
|
{ name: "qBittorrent", image: "home-oper./qbittorrent", version: "5.1.4", ports: ["30024","51413/tcp+udp"], status: "up", uptime: "5 мин", icon: "🌊", category: "Медиа" },
|
||||||
|
{ name: "Ollama", image: "ollama/ollama", version: "0.22.0", ports: ["11434"], status: "up", uptime: "5 мин", icon: "🤖", category: "AI" },
|
||||||
|
{ name: "Open WebUI", image: "open-webui", version: "v0.9.2-ollama", ports: ["3000"], status: "up", uptime: "5 ч", icon: "💬", category: "AI" },
|
||||||
|
{ name: "Nextcloud", image: "ix-nextcloud", version: "33.0.2", ports: ["30027"], status: "up", uptime: "20 ч", icon: "☁️", category: "Хранилище" },
|
||||||
|
{ name: "Immich", image: "immich-server", version: "v2.7.5", ports: ["30041"], status: "up", uptime: "20 ч", icon: "📷", category: "Медиа" },
|
||||||
|
{ name: "WG-Easy", image: "wg-easy", version: "15.2.2", ports: ["30058"], status: "up", uptime: "20 ч", icon: "🔒", category: "Сеть" },
|
||||||
|
{ name: "Vaultwarden", image: "vaultwarden/server", version: "1.35.8", ports: ["30032"], status: "up", uptime: "20 ч", icon: "🔑", category: "Безопасность" },
|
||||||
|
{ name: "Jellyfin", image: "jellyfin/jellyfin", version: "10.11.8", ports: ["8096"], status: "up", uptime: "20 ч", icon: "🎬", category: "Медиа" },
|
||||||
|
{ name: "Heimdall", image: "linuxserver/heimdall", version: "2.7.6", ports: ["30119","30120"], status: "up", uptime: "20 ч", icon: "🏠", category: "Утилиты" },
|
||||||
|
{ name: "Gitea", image: "gitea/gitea", version: "1.26.1-rootless", ports: ["30008","30009"], status: "up", uptime: "20 ч", icon: "🐱", category: "Dev" },
|
||||||
|
{ name: "Frigate", image: "frigate", version: "0.17.1", ports: ["30193","30194"], status: "up", uptime: "20 ч", icon: "📹", category: "Медиа" },
|
||||||
|
{ name: "Dockge", image: "louislam/dockge", version: "1.5.0", ports: ["31014"], status: "up", uptime: "20 ч", icon: "🐳", category: "Dev" },
|
||||||
|
{ name: "Termix", image: "lukegus/termix", version: "release-2.1.0", ports: ["30351"], status: "up", uptime: "20 ч", icon: "💻", category: "Dev" },
|
||||||
|
{ name: "OnlyOffice", image: "onlyoffice/documentserver", version: "9.3.1", ports: ["30134"], status: "up", uptime: "20 ч", icon: "📝", category: "Утилиты" },
|
||||||
|
{ name: "Mail Archiver", image: "s1t5/mailarchiver", version: "2604.2", ports: ["30315"], status: "up", uptime: "20 ч", icon: "📧", category: "Утилиты" },
|
||||||
|
],
|
||||||
|
dockge: [
|
||||||
|
{ name: "Beszel Agent", image: "henrygd/beszel-agent", version: "latest", ports: ["—"], status: "up", uptime: "4 мин", icon: "📡", category: "Мониторинг" },
|
||||||
|
{ name: "Dockmon", image: "darthnorse/dockmon", version: "latest", ports: ["8001"], status: "up", uptime: "20 ч", icon: "👁️", category: "Мониторинг" },
|
||||||
|
{ name: "TorrServer", image: "yourok/torrserver", version: "latest", ports: ["8090"], status: "up", uptime: "20 ч", icon: "🌊", category: "Медиа" },
|
||||||
|
{ name: "Prunemate", image: "anoniemerd/prunemate", version: "latest", ports: ["7676"], status: "up", uptime: "20 ч", icon: "✂️", category: "Утилиты" },
|
||||||
|
{ name: "OmniRoute", image: "diegosouzapw/omniroute", version: "latest", ports: ["—"], status: "exited", uptime: "11 дн назад", icon: "🔀", category: "Сеть" },
|
||||||
|
{ name: "Code-Server", image: "linuxserver/code-server", version: "latest", ports: ["—"], status: "exited", uptime: "11 дн назад", icon: "🖊️", category: "Dev" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
proxmox1: [
|
||||||
|
{ name: "Home Assistant", type: "VM", ip: "192.168.31.10", icon: "🏡", status: "up", note: "БД → MariaDB LXC" },
|
||||||
|
{ name: "AdGuard Home", type: "LXC", ip: "192.168.31.3", icon: "🛡️", status: "up", note: "DNS → Keenetic Ultra" },
|
||||||
|
{ name: "MariaDB", type: "LXC", ip: "192.168.31.21", icon: "🗄️", status: "up", note: "БД для Home Assistant" },
|
||||||
|
],
|
||||||
|
servers: [
|
||||||
|
{ name: "TrueNAS", model: "WTR PRO", cpu: "Ryzen 7 5825U (16T)", ram: "64 GiB", disk: "~7.5 TiB полезных", ip: "192.168.31.100", os: "TrueNAS CE 25.10.3", status: "up", icon: "🔷" },
|
||||||
|
{ name: "Mini PC #1", model: "Celeron", cpu: "Celeron J4125 (4C)", ram: "15.46 GiB", disk: "38.64 GiB", ip: "192.168.31.2", os: "Proxmox VE 9.1.9", status: "up", icon: "🔶" },
|
||||||
|
{ name: "Mini PC #2", model: "Ryzen", cpu: "Ryzen 7 5825U (16T)", ram: "28.29 GiB", disk: "93.93 GiB (59%⚠️)", ip: "192.168.1.10", os: "Proxmox VE 9.1.9", status: "up", icon: "🔴" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const categories = ["Все", "AI", "БД", "Безопасность", "Dev", "Коммуникации", "Медиа", "Мониторинг", "Сеть", "Утилиты", "Хранилище"];
|
||||||
|
|
||||||
|
const categoryColors = {
|
||||||
|
AI: "#a78bfa", БД: "#f59e0b", Безопасность: "#ef4444", Dev: "#3b82f6",
|
||||||
|
Коммуникации: "#06b6d4", Медиа: "#ec4899", Мониторинг: "#10b981",
|
||||||
|
Сеть: "#8b5cf6", Утилиты: "#6b7280", Хранилище: "#f97316",
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatusBadge({ status }) {
|
||||||
|
const isUp = status === "up";
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
display: "inline-flex", alignItems: "center", gap: 5,
|
||||||
|
padding: "2px 8px", borderRadius: 4, fontSize: 11, fontWeight: 700,
|
||||||
|
letterSpacing: "0.05em", textTransform: "uppercase",
|
||||||
|
background: isUp ? "rgba(16,185,129,0.15)" : "rgba(107,114,128,0.15)",
|
||||||
|
color: isUp ? "#10b981" : "#6b7280",
|
||||||
|
border: `1px solid ${isUp ? "rgba(16,185,129,0.3)" : "rgba(107,114,128,0.3)"}`,
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
width: 6, height: 6, borderRadius: "50%",
|
||||||
|
background: isUp ? "#10b981" : "#6b7280",
|
||||||
|
boxShadow: isUp ? "0 0 6px #10b981" : "none",
|
||||||
|
animation: isUp ? "pulse 2s infinite" : "none",
|
||||||
|
}} />
|
||||||
|
{isUp ? "UP" : "EXIT"}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CatBadge({ cat }) {
|
||||||
|
const color = categoryColors[cat] || "#6b7280";
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
padding: "1px 7px", borderRadius: 3, fontSize: 10, fontWeight: 600,
|
||||||
|
background: color + "22", color, border: `1px solid ${color}44`,
|
||||||
|
letterSpacing: "0.04em",
|
||||||
|
}}>{cat}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PortBadge({ port }) {
|
||||||
|
return (
|
||||||
|
<span style={{
|
||||||
|
display: "inline-block", padding: "1px 6px", margin: "1px 2px",
|
||||||
|
borderRadius: 3, fontSize: 11, fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
background: "rgba(59,130,246,0.12)", color: "#60a5fa",
|
||||||
|
border: "1px solid rgba(59,130,246,0.25)",
|
||||||
|
}}>{port}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [tab, setTab] = useState("apps");
|
||||||
|
const [cat, setCat] = useState("Все");
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const allApps = [...data.truenas.apps, ...data.truenas.dockge];
|
||||||
|
|
||||||
|
const filtered = allApps.filter(a => {
|
||||||
|
const matchCat = cat === "Все" || a.category === cat;
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
const matchSearch = !q || a.name.toLowerCase().includes(q) || a.image.toLowerCase().includes(q) || a.category.toLowerCase().includes(q);
|
||||||
|
return matchCat && matchSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "apps", label: "🐳 Контейнеры" },
|
||||||
|
{ id: "proxmox", label: "🔶 Proxmox #1" },
|
||||||
|
{ id: "servers", label: "🖥️ Серверы" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
minHeight: "100vh", background: "#090d13",
|
||||||
|
fontFamily: "'Space Mono', 'JetBrains Mono', monospace",
|
||||||
|
color: "#c9d1d9", padding: "24px 20px",
|
||||||
|
}}>
|
||||||
|
<style>{`
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@700;800&display=swap');
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||||
|
::-webkit-scrollbar-track { background: #111; }
|
||||||
|
::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
|
||||||
|
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
|
||||||
|
@keyframes fadeIn { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:none} }
|
||||||
|
tr { animation: fadeIn 0.2s ease both; }
|
||||||
|
.tab-btn { transition: all 0.15s; }
|
||||||
|
.tab-btn:hover { background: rgba(255,255,255,0.06) !important; }
|
||||||
|
.row:hover td { background: rgba(255,255,255,0.03) !important; }
|
||||||
|
.cat-btn { transition: all 0.15s; cursor: pointer; }
|
||||||
|
.cat-btn:hover { opacity: 0.85; }
|
||||||
|
input::placeholder { color: #444; }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ marginBottom: 28, borderBottom: "1px solid #1e2d3d", paddingBottom: 20 }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "flex-end", gap: 16, flexWrap: "wrap" }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 11, color: "#3b82f6", letterSpacing: "0.15em", marginBottom: 4 }}>
|
||||||
|
WTR HOMELAB · 192.168.31.0/24 ↔ 192.168.1.0/24
|
||||||
|
</div>
|
||||||
|
<h1 style={{
|
||||||
|
margin: 0, fontSize: 26, fontFamily: "'Syne', sans-serif",
|
||||||
|
fontWeight: 800, color: "#e6edf3", letterSpacing: "-0.02em",
|
||||||
|
}}>
|
||||||
|
Infrastructure Dashboard
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginLeft: "auto", display: "flex", gap: 16, flexWrap: "wrap" }}>
|
||||||
|
{data.servers.map(s => (
|
||||||
|
<div key={s.name} style={{
|
||||||
|
padding: "6px 12px", borderRadius: 6,
|
||||||
|
background: "rgba(16,185,129,0.08)", border: "1px solid rgba(16,185,129,0.2)",
|
||||||
|
fontSize: 11,
|
||||||
|
}}>
|
||||||
|
<span style={{ color: "#10b981" }}>●</span>{" "}
|
||||||
|
<span style={{ color: "#8b949e" }}>{s.name}</span>{" "}
|
||||||
|
<span style={{ color: "#c9d1d9", fontWeight: 700 }}>{s.ip}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div style={{ display: "flex", gap: 4, marginBottom: 20 }}>
|
||||||
|
{tabs.map(t => (
|
||||||
|
<button key={t.id} className="tab-btn"
|
||||||
|
onClick={() => setTab(t.id)}
|
||||||
|
style={{
|
||||||
|
padding: "8px 16px", border: "none", borderRadius: 6, cursor: "pointer",
|
||||||
|
fontFamily: "inherit", fontSize: 12, fontWeight: 700,
|
||||||
|
background: tab === t.id ? "#1e2d3d" : "transparent",
|
||||||
|
color: tab === t.id ? "#60a5fa" : "#6b7280",
|
||||||
|
borderBottom: tab === t.id ? "2px solid #3b82f6" : "2px solid transparent",
|
||||||
|
}}>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CONTAINERS TAB */}
|
||||||
|
{tab === "apps" && (
|
||||||
|
<div style={{ animation: "fadeIn 0.25s ease" }}>
|
||||||
|
{/* Filters */}
|
||||||
|
<div style={{ display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap", alignItems: "center" }}>
|
||||||
|
<input
|
||||||
|
value={search} onChange={e => setSearch(e.target.value)}
|
||||||
|
placeholder="🔍 поиск по сервису или образу..."
|
||||||
|
style={{
|
||||||
|
background: "#0d1117", border: "1px solid #21262d", borderRadius: 6,
|
||||||
|
color: "#c9d1d9", padding: "7px 12px", fontSize: 12, fontFamily: "inherit",
|
||||||
|
width: 260, outline: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
||||||
|
{categories.map(c => (
|
||||||
|
<button key={c} className="cat-btn"
|
||||||
|
onClick={() => setCat(c)}
|
||||||
|
style={{
|
||||||
|
padding: "4px 10px", border: "none", borderRadius: 4, fontSize: 11,
|
||||||
|
fontFamily: "inherit", fontWeight: 600,
|
||||||
|
background: cat === c
|
||||||
|
? (categoryColors[c] ? categoryColors[c] + "33" : "rgba(255,255,255,0.12)")
|
||||||
|
: "rgba(255,255,255,0.04)",
|
||||||
|
color: cat === c ? (categoryColors[c] || "#fff") : "#6b7280",
|
||||||
|
border: cat === c
|
||||||
|
? `1px solid ${categoryColors[c] ? categoryColors[c] + "66" : "#555"}`
|
||||||
|
: "1px solid transparent",
|
||||||
|
}}>
|
||||||
|
{c}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span style={{ marginLeft: "auto", fontSize: 11, color: "#444" }}>
|
||||||
|
{filtered.length} / {allApps.length} сервисов
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section: ix-apps */}
|
||||||
|
{(() => {
|
||||||
|
const ixFiltered = filtered.filter(a => data.truenas.apps.includes(a));
|
||||||
|
const dockgeFiltered = filtered.filter(a => data.truenas.dockge.includes(a));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{ixFiltered.length > 0 && (
|
||||||
|
<SectionTable title="TrueNAS Scale Apps (ix-*)" items={ixFiltered} />
|
||||||
|
)}
|
||||||
|
{dockgeFiltered.length > 0 && (
|
||||||
|
<SectionTable title="Dockge / Docker Compose (standalone)" items={dockgeFiltered} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* PROXMOX TAB */}
|
||||||
|
{tab === "proxmox" && (
|
||||||
|
<div style={{ animation: "fadeIn 0.25s ease" }}>
|
||||||
|
<div style={{
|
||||||
|
marginBottom: 12, padding: "10px 14px", borderRadius: 6,
|
||||||
|
background: "#0d1117", border: "1px solid #21262d", fontSize: 11, color: "#8b949e",
|
||||||
|
}}>
|
||||||
|
🔶 Mini PC #1 · Celeron J4125 · 15.46 GiB RAM · 38.64 GiB Disk ·{" "}
|
||||||
|
<span style={{ color: "#60a5fa" }}>192.168.31.2</span> · Proxmox VE 9.1.9
|
||||||
|
</div>
|
||||||
|
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{["Сервис", "Тип", "IP-адрес", "Статус", "Примечание"].map(h => (
|
||||||
|
<th key={h} style={{
|
||||||
|
textAlign: "left", padding: "9px 12px", fontSize: 10,
|
||||||
|
color: "#3b82f6", letterSpacing: "0.1em", textTransform: "uppercase",
|
||||||
|
borderBottom: "1px solid #1e2d3d", fontWeight: 700,
|
||||||
|
}}>{h}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data.proxmox1.map((s, i) => (
|
||||||
|
<tr key={i} className="row">
|
||||||
|
<td style={{ padding: "10px 12px", borderBottom: "1px solid #161b22" }}>
|
||||||
|
<span style={{ marginRight: 8 }}>{s.icon}</span>
|
||||||
|
<span style={{ color: "#e6edf3", fontWeight: 700 }}>{s.name}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "10px 12px", borderBottom: "1px solid #161b22" }}>
|
||||||
|
<span style={{
|
||||||
|
padding: "2px 7px", borderRadius: 3, fontSize: 11,
|
||||||
|
background: s.type === "VM" ? "rgba(168,85,247,0.15)" : "rgba(251,191,36,0.12)",
|
||||||
|
color: s.type === "VM" ? "#a855f7" : "#fbbf24",
|
||||||
|
border: `1px solid ${s.type === "VM" ? "rgba(168,85,247,0.3)" : "rgba(251,191,36,0.25)"}`,
|
||||||
|
}}>{s.type}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "10px 12px", borderBottom: "1px solid #161b22", color: "#60a5fa", fontFamily: "monospace" }}>
|
||||||
|
{s.ip}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "10px 12px", borderBottom: "1px solid #161b22" }}>
|
||||||
|
<StatusBadge status={s.status} />
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "10px 12px", borderBottom: "1px solid #161b22", color: "#8b949e", fontSize: 12 }}>
|
||||||
|
{s.note}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* SERVERS TAB */}
|
||||||
|
{tab === "servers" && (
|
||||||
|
<div style={{ animation: "fadeIn 0.25s ease" }}>
|
||||||
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 14 }}>
|
||||||
|
{data.servers.map((s, i) => (
|
||||||
|
<div key={i} style={{
|
||||||
|
background: "#0d1117", border: "1px solid #21262d", borderRadius: 8,
|
||||||
|
padding: 18, position: "relative", overflow: "hidden",
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
position: "absolute", top: 0, left: 0, right: 0, height: 2,
|
||||||
|
background: i === 0 ? "#3b82f6" : i === 1 ? "#f59e0b" : "#ef4444",
|
||||||
|
}} />
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
|
||||||
|
<span style={{ fontSize: 22 }}>{s.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontFamily: "'Syne',sans-serif", fontWeight: 800, fontSize: 15, color: "#e6edf3" }}>{s.name}</div>
|
||||||
|
<div style={{ fontSize: 10, color: "#6b7280" }}>{s.os}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginLeft: "auto" }}><StatusBadge status={s.status} /></div>
|
||||||
|
</div>
|
||||||
|
{[
|
||||||
|
["IP", s.ip], ["CPU", s.cpu], ["RAM", s.ram], ["Диск", s.disk],
|
||||||
|
].map(([k, v]) => (
|
||||||
|
<div key={k} style={{
|
||||||
|
display: "flex", justifyContent: "space-between", alignItems: "center",
|
||||||
|
padding: "5px 0", borderBottom: "1px solid #161b22", fontSize: 12,
|
||||||
|
}}>
|
||||||
|
<span style={{ color: "#6b7280", fontSize: 10, letterSpacing: "0.08em" }}>{k}</span>
|
||||||
|
<span style={{ color: k === "IP" ? "#60a5fa" : "#c9d1d9", fontFamily: "monospace" }}>{v}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Network */}
|
||||||
|
<div style={{ marginTop: 20, background: "#0d1117", border: "1px solid #21262d", borderRadius: 8, padding: 18 }}>
|
||||||
|
<div style={{ fontSize: 10, color: "#3b82f6", letterSpacing: "0.12em", marginBottom: 12 }}>🌐 СЕТЕВАЯ ТОПОЛОГИЯ</div>
|
||||||
|
<div style={{ display: "flex", gap: 12, flexWrap: "wrap", alignItems: "center", fontSize: 12 }}>
|
||||||
|
<div style={{ padding: "8px 14px", borderRadius: 6, background: "#0a1628", border: "1px solid #1e3a5f", textAlign: "center" }}>
|
||||||
|
<div style={{ color: "#60a5fa", fontWeight: 700, marginBottom: 2 }}>Keenetic Ultra</div>
|
||||||
|
<div style={{ color: "#6b7280", fontSize: 10 }}>NC-1812 · WG Client</div>
|
||||||
|
<div style={{ color: "#e6edf3", fontFamily: "monospace", fontSize: 11 }}>192.168.31.1</div>
|
||||||
|
<div style={{ color: "#8b949e", fontSize: 10, marginTop: 2 }}>DNS → 192.168.31.3</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 18, color: "#3b82f6" }}>⟷</div>
|
||||||
|
<div style={{ padding: "6px 12px", borderRadius: 6, background: "rgba(59,130,246,0.08)", border: "1px dashed rgba(59,130,246,0.3)", fontSize: 11, color: "#60a5fa" }}>
|
||||||
|
WireGuard VPN
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 18, color: "#3b82f6" }}>⟷</div>
|
||||||
|
<div style={{ padding: "8px 14px", borderRadius: 6, background: "#0a1628", border: "1px solid #1e3a5f", textAlign: "center" }}>
|
||||||
|
<div style={{ color: "#60a5fa", fontWeight: 700, marginBottom: 2 }}>Keenetic Giga</div>
|
||||||
|
<div style={{ color: "#6b7280", fontSize: 10 }}>KN-1011 · WG Server</div>
|
||||||
|
<div style={{ color: "#e6edf3", fontFamily: "monospace", fontSize: 11 }}>192.168.1.1</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SectionTable({ title, items }) {
|
||||||
|
return (
|
||||||
|
<div style={{ marginBottom: 28 }}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 10, color: "#3b82f6", letterSpacing: "0.12em",
|
||||||
|
marginBottom: 8, textTransform: "uppercase", fontWeight: 700,
|
||||||
|
}}>
|
||||||
|
{title} <span style={{ color: "#444", fontWeight: 400 }}>({items.length})</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ overflowX: "auto" }}>
|
||||||
|
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{["Сервис", "Образ", "Версия", "Порты", "Категория", "Статус", "Uptime"].map(h => (
|
||||||
|
<th key={h} style={{
|
||||||
|
textAlign: "left", padding: "8px 12px", fontSize: 10,
|
||||||
|
color: "#3b82f6", letterSpacing: "0.1em", textTransform: "uppercase",
|
||||||
|
borderBottom: "1px solid #1e2d3d", fontWeight: 700, whiteSpace: "nowrap",
|
||||||
|
}}>{h}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((a, i) => (
|
||||||
|
<tr key={i} className="row" style={{ animationDelay: `${i * 0.03}s` }}>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22", whiteSpace: "nowrap" }}>
|
||||||
|
<span style={{ marginRight: 7 }}>{a.icon}</span>
|
||||||
|
<span style={{ color: "#e6edf3", fontWeight: 700 }}>{a.name}</span>
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22", color: "#8b949e", fontSize: 11, fontFamily: "monospace" }}>
|
||||||
|
{a.image}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22", color: "#58a6ff", fontSize: 11, fontFamily: "monospace", whiteSpace: "nowrap" }}>
|
||||||
|
{a.version}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22" }}>
|
||||||
|
{a.ports.map((p, pi) => <PortBadge key={pi} port={p} />)}
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22" }}>
|
||||||
|
<CatBadge cat={a.category} />
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22" }}>
|
||||||
|
<StatusBadge status={a.status} />
|
||||||
|
</td>
|
||||||
|
<td style={{ padding: "9px 12px", borderBottom: "1px solid #161b22", color: "#6b7280", fontSize: 11, whiteSpace: "nowrap" }}>
|
||||||
|
{a.uptime}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user