diff --git a/app/(main)/containers/page.tsx b/app/(main)/containers/page.tsx index 9157e681..cf5a08a3 100644 --- a/app/(main)/containers/page.tsx +++ b/app/(main)/containers/page.tsx @@ -72,94 +72,33 @@ function statusConfig(status: ContainerView["status"]) { return { unknown: { dot: "bg-muted-foreground", badge: "bg-muted text-muted-foreground border-border", label: "Неизвестно" }, running: { - dot: "bg-emerald-500 animate-pulse", - badge: "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20", - label: "Running", + dot: "bg-success", + badge: "bg-success/10 text-success border-success/20", + label: "Работает", }, stopped: { dot: "bg-muted-foreground", badge: "bg-muted/50 text-muted-foreground border-border", - label: "Stopped", + label: "Остановлен", }, error: { - dot: "bg-red-500 animate-pulse", - badge: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20", - label: "Error", + dot: "bg-destructive", + badge: "bg-destructive/10 text-destructive border-destructive/20", + label: "Ошибка", }, }[status] } -function generateContainerRsc(c: ContainerView, serverById: Record): string { - const srv = serverById[c.serverId] - const lines: string[] = [] - lines.push(`# RouterOS Container — ${c.name}`) - if (srv) lines.push(`# Сервер: ${srv.name} (${srv.host})`) - lines.push(`# Образ: ${c.image}:${c.tag}`) - lines.push(`# RouterOS 7.4+ · /container`) - lines.push(``) - - for (const iface of c.interfaces) { - lines.push(`/interface/veth/add name=${iface} address=172.17.0.2/24 gateway=172.17.0.1`) - } - lines.push(``) - - if (c.envs.length > 0) { - lines.push(`/container/envs/add name=${c.name}-envs \\`) - for (const { key, value } of c.envs) { - lines.push(` ${key}="${value}" \\`) - } - lines.push(``) - } - - for (const m of c.mounts) { - lines.push(`/container/mounts/add name=${c.name}-mount-${m.dst.replace(/\//g, "-").slice(1)} \\`) - if (m.src) lines.push(` src=${m.src} \\`) - lines.push(` dst=${m.dst}`) - lines.push(``) - } - - lines.push(`/container/add \\`) - lines.push(` remote-image=${c.image}:${c.tag} \\`) - lines.push(` interface=${c.interfaces[0] ?? "veth-container"} \\`) - if (c.envs.length > 0) lines.push(` envlist=${c.name}-envs \\`) - if (c.mounts.length > 0) lines.push(` mounts=${c.mounts.map((m) => `${c.name}-mount-${m.dst.replace(/\//g, "-").slice(1)}`).join(",")} \\`) - if (c.cmd) lines.push(` cmd="${c.cmd}" \\`) - if (c.startOnBoot) lines.push(` start-on-boot=yes \\`) - if (c.comment) lines.push(` comment="${c.comment}" \\`) - lines.push(` logging=yes`) - - return lines.join("\n") -} - -function ExportSheet({ - open, container, onClose, serverById, -}: { - open: boolean - container: ContainerView | null - onClose: () => void - serverById: Record -}) { - const code = useMemo( - () => (container ? generateContainerRsc(container, serverById) : ""), - [container, serverById], - ) - - return ( - - ) +type ContainerExport = { container: ContainerView; collectedAt: string | null; failures: ContainerServerState["failures"]; source: "live" | "mock" } +function ExportSheet({ selection, onClose }: {selection: ContainerExport | null; onClose: () => void}) { + const formats = useMemo(() => [{ + id: "json", label: "Снимок JSON", filename: "container-" + (selection?.container.id ?? "snapshot").replace(/[^a-zA-Z0-9_-]/g, "_") + ".json", + code: selection ? JSON.stringify(selection, null, 2) : "", + }], [selection]) + return {selection?.source === "mock" ? "Демонстрационные данные." : "Чтение RouterOS: " + (selection?.collectedAt ? new Date(selection.collectedAt).toLocaleString("ru-RU") : "время неизвестно")}{selection?.failures.length ? " Настройки получены частично; ошибки включены в JSON." : ""}

} + formats={formats} /> } function ContainerCard({ @@ -199,26 +138,26 @@ function ContainerCard({ - + } /> {container.status === "running" ? ( - Остановить + ) : ( - - Запустить + + )} - Логи - Редактировать - Экспорт .rsc + + + - - Перезапустить + + @@ -230,17 +169,17 @@ function ContainerCard({
- - +
{server && (
- +
)} @@ -248,7 +187,7 @@ function ContainerCard({
{container.uptime && (
- +
)} @@ -256,7 +195,7 @@ function ContainerCard({
CPU 50 ? "text-amber-500" : "text-foreground")}> + container.cpu > 50 ? "text-warning" : "text-foreground")}> {container.cpu}%
@@ -264,7 +203,7 @@ function ContainerCard({ {container.memMb !== undefined && (
RAM - {container.memMb} МБ + {container.memMb} МиБ
)}
@@ -293,7 +232,7 @@ function ContainerCard({ )} {container.comment && ( -

{container.comment}

+

{container.comment}

)}
@@ -314,7 +253,7 @@ function ContainersContent() { const canWrite = can("mm:servers:write") const [search, setSearch] = useState("") const [statusFilter, setStatusFilter] = useState("all") - const [exportContainer, setExportContainer] = useState(null) + const [exportContainer, setExportContainer] = useState(null) const [selectedServerId, setSelectedServerId] = useState(ALL_SERVERS_ID) const [state, setState] = useState(null) const [lastStates, setLastStates] = useState>({}) @@ -412,6 +351,7 @@ function ContainersContent() { const running = scoped.filter((c) => c.status === "running").length const stopped = scoped.filter((c) => c.status === "stopped").length const errors = scoped.filter((c) => c.status === "error").length + const statusKnown = known && scoped.every(container => container.status !== "unknown") async function mutate(c: ContainerView, action: ContainerAction) { if (!isLive || !c.rosId || !canWrite || !nodeFresh(c.serverId) || mutationLock.current || !displayContainers.some(row => row.id === c.id && row.rosId === c.rosId && row.serverId === c.serverId)) return @@ -465,20 +405,20 @@ function ContainersContent() { onClick={() => { void loadLive() }} disabled={!isLive || loading || !canRead && !canReadServers || !!busyId} > - +