Compare commits

...

3 Commits

Author SHA1 Message Date
MengMengCode c99f3f6d55 release: v1.0.12 2026-06-07 13:16:39 +08:00
MengMengCode 2df92be501 修复了一些已知问题 2026-06-07 13:15:59 +08:00
MengMengCode f8d16ca792 添加标签 2026-06-07 10:55:52 +08:00
14 changed files with 126 additions and 24 deletions
+1
View File
@@ -64,3 +64,4 @@ backend/tmp/
Thumbs.db Thumbs.db
linux.txt linux.txt
push-release.ps1 push-release.ps1
deploy.ps1
+4 -3
View File
@@ -11,9 +11,10 @@
<img alt="Vite" src="https://img.shields.io/badge/Vite-5-646CFF?style=flat-square&logo=vite&logoColor=white"> <img alt="Vite" src="https://img.shields.io/badge/Vite-5-646CFF?style=flat-square&logo=vite&logoColor=white">
<img alt="Tailwind CSS" src="https://img.shields.io/badge/Tailwind_CSS-3-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white"> <img alt="Tailwind CSS" src="https://img.shields.io/badge/Tailwind_CSS-3-06B6D4?style=flat-square&logo=tailwindcss&logoColor=white">
<img alt="LXC" src="https://img.shields.io/badge/LXC-container-111111?style=flat-square"> <img alt="LXC" src="https://img.shields.io/badge/LXC-container-111111?style=flat-square">
<img alt="KVM" src="https://img.shields.io/badge/KVM-virtualization-EE0000?style=flat-square&logo=linux&logoColor=white">
</p> </p>
CLICD 是一个面向 LXC 的轻量容器管理面板,提供 Web 控制台、CLI、批量任务、镜像管理、NAT 端口、IPv6 分配、WebSSH、资源限制、流量限制和安全告警能力。它适合用来管理小型 VPS 上的 LXC 容器,也适合需要批量创建和分发子用户管理链接的场景。 CLICD 是一个面向 LXC/KVM 的轻量虚拟化管理面板,提供 Web 控制台、CLI、批量任务、镜像管理、NAT 端口、IPv6 分配、WebSSH、VNC、资源限制、流量限制和安全告警能力。它适合用来管理小型 VPS 上的 LXC 容器和 KVM 虚拟机,也适合需要批量创建和分发子用户管理链接的场景。
## 功能介绍 ## 功能介绍
@@ -29,9 +30,9 @@ CLICD 是一个面向 LXC 的轻量容器管理面板,提供 Web 控制台、C
## 技术栈 ## 技术栈
- Backend: Go, net/http, LXC, cgroup v2, iptables, conntrack - Backend: Go, net/http, LXC, KVM/libvirt, cgroup v2, iptables, conntrack
- Frontend: React, TypeScript, Vite, Tailwind CSS, lucide-react, xterm.js - Frontend: React, TypeScript, Vite, Tailwind CSS, lucide-react, xterm.js
- Runtime: Linux, systemd, LXC - Runtime: Linux, systemd, LXC, KVM/QEMU
- Build: GitHub Actions, Node.js 20, Go 1.22 - Build: GitHub Actions, Node.js 20, Go 1.22
## 安装 ## 安装
+5 -1
View File
@@ -298,7 +298,11 @@ func updateResourceLimit(w http.ResponseWriter, r *http.Request, id int) {
} }
} }
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: "Resource limits updated"}) msg := "Resource limits updated"
if c.IsKVM() && c.Status == "running" {
msg = "资源已保存,请关机重启虚拟机后生效"
}
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Message: msg})
} }
func getRandomPort(w http.ResponseWriter, r *http.Request, id int) { func getRandomPort(w http.ResponseWriter, r *http.Request, id int) {
+17 -2
View File
@@ -716,14 +716,29 @@ func UpdateVNC(containers []Container) {
SaveConfig() SaveConfig()
} }
// AllocateSSHPort allocates a new SSH port // AllocateSSHPort allocates a new SSH port, skipping ports already used by any container
func AllocateSSHPort() int { func AllocateSSHPort() int {
used := collectAllHostPorts()
port := AppConfig.NextSSHPort port := AppConfig.NextSSHPort
AppConfig.NextSSHPort++ for used[port] {
port++
}
AppConfig.NextSSHPort = port + 1
SaveConfig() SaveConfig()
return port return port
} }
// collectAllHostPorts collects all host ports used by any container (LXC + KVM)
func collectAllHostPorts() map[int]bool {
used := map[int]bool{}
for _, c := range AppConfig.Containers {
for _, pm := range c.PortMappings {
used[pm.HostPort] = true
}
}
return used
}
// IsValidContainerName checks if container name is valid (no duplicate check needed, ID is primary key) // IsValidContainerName checks if container name is valid (no duplicate check needed, ID is primary key)
func IsValidContainerName(name string) bool { func IsValidContainerName(name string) bool {
return IsValidContainerNameSyntax(name) return IsValidContainerNameSyntax(name)
+15 -1
View File
@@ -503,7 +503,8 @@ func (m *Manager) ApplyContainerLimits(c *config.Container) error {
return nil return nil
} }
if c.Status == "running" { if c.Status == "running" {
return fmt.Errorf("KVM resource changes require shutdown and start") // Config already saved; domain definition will be refreshed on next start
return nil
} }
if c.DiskImage == "" || c.MACAddress == "" { if c.DiskImage == "" || c.MACAddress == "" {
return nil return nil
@@ -901,6 +902,9 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
"disk_write_bytes": writeBytes, "disk_write_bytes": writeBytes,
"disk_read_bps": 0.0, "disk_read_bps": 0.0,
"disk_write_bps": 0.0, "disk_write_bps": 0.0,
"load1": 0.0,
"load5": 0.0,
"load15": 0.0,
} }
if c.DiskImage != "" { if c.DiskImage != "" {
if info, err := os.Stat(c.DiskImage); err == nil { if info, err := os.Stat(c.DiskImage); err == nil {
@@ -2264,10 +2268,20 @@ func allocateDefaultEqualPorts(c *config.Container, count int) []int {
return nil return nil
} }
used := map[int]bool{} used := map[int]bool{}
// Mark current container's ports
for _, pm := range c.PortMappings { for _, pm := range c.PortMappings {
used[pm.HostPort] = true used[pm.HostPort] = true
used[pm.ContainerPort] = true used[pm.ContainerPort] = true
} }
// Also mark all other containers' host ports (LXC + KVM)
for _, oc := range config.AppConfig.Containers {
if oc.ID == c.ID {
continue
}
for _, pm := range oc.PortMappings {
used[pm.HostPort] = true
}
}
ports := make([]int, 0, count) ports := make([]int, 0, count)
for next := 20000; next <= 65535 && len(ports) < count; next++ { for next := 20000; next <= 65535 && len(ports) < count; next++ {
if !used[next] { if !used[next] {
+24
View File
@@ -2272,6 +2272,11 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
usage := make(map[string]interface{}) usage := make(map[string]interface{})
// Read raw values // Read raw values
load1, load5, load15 := m.getContainerLoadAvg(lxcName)
usage["load1"] = load1
usage["load5"] = load5
usage["load15"] = load15
memUsage := readIntCommand(fmt.Sprintf( memUsage := readIntCommand(fmt.Sprintf(
"cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+ "cat /sys/fs/cgroup/lxc/%[1]s/memory.current 2>/dev/null || "+
"cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+ "cat /sys/fs/cgroup/lxc.payload.%[1]s/memory.current 2>/dev/null || "+
@@ -2319,6 +2324,25 @@ func (m *Manager) GetResourceUsage(id int) (map[string]interface{}, error) {
return usage, nil return usage, nil
} }
func (m *Manager) getContainerLoadAvg(lxcName string) (float64, float64, float64) {
pid := m.getContainerInitPID(lxcName)
if pid == "" {
return 0, 0, 0
}
out, err := exec.Command("nsenter", "-t", pid, "-m", "-p", "cat", "/proc/loadavg").Output()
if err != nil {
return 0, 0, 0
}
parts := strings.Fields(string(out))
if len(parts) < 3 {
return 0, 0, 0
}
load1, _ := strconv.ParseFloat(parts[0], 64)
load5, _ := strconv.ParseFloat(parts[1], 64)
load15, _ := strconv.ParseFloat(parts[2], 64)
return load1, load5, load15
}
func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) { func (m *Manager) getContainerNetworkBytes(lxcName string) (uint64, uint64) {
pid := m.getContainerInitPID(lxcName) pid := m.getContainerInitPID(lxcName)
if pid == "" { if pid == "" {
+23 -1
View File
@@ -174,12 +174,24 @@ func normalizePortMapping(c *config.Container, skipIndex int, pm config.PortMapp
if pm.HostPort <= 0 { if pm.HostPort <= 0 {
pm.HostPort = pm.ContainerPort pm.HostPort = pm.ContainerPort
} }
// Check current container's own mappings
for i, existing := range c.PortMappings { for i, existing := range c.PortMappings {
if i == skipIndex { if i == skipIndex {
continue continue
} }
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol { if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
return pm, fmt.Errorf("host port %d/%s already mapped", pm.HostPort, pm.Protocol) return pm, fmt.Errorf("host port %d/%s already mapped in this container", pm.HostPort, pm.Protocol)
}
}
// Check all other containers (LXC + KVM) for port conflicts
for _, oc := range config.AppConfig.Containers {
if oc.ID == c.ID {
continue
}
for _, existing := range oc.PortMappings {
if existing.HostPort == pm.HostPort && existing.Protocol == pm.Protocol {
return pm, fmt.Errorf("host port %d/%s already used by container %s (ID: %d)", pm.HostPort, pm.Protocol, oc.Name, oc.ID)
}
} }
} }
return pm, nil return pm, nil
@@ -190,10 +202,20 @@ func allocateDefaultEqualPorts(c *config.Container, count int) []int {
return nil return nil
} }
used := map[int]bool{} used := map[int]bool{}
// Mark current container's ports
for _, pm := range c.PortMappings { for _, pm := range c.PortMappings {
used[pm.HostPort] = true used[pm.HostPort] = true
used[pm.ContainerPort] = true used[pm.ContainerPort] = true
} }
// Also mark all other containers' host ports (LXC + KVM)
for _, oc := range config.AppConfig.Containers {
if oc.ID == c.ID {
continue
}
for _, pm := range oc.PortMappings {
used[pm.HostPort] = true
}
}
ports := make([]int, 0, count) ports := make([]int, 0, count)
next := 20000 next := 20000
for len(ports) < count { for len(ports) < count {
+1
View File
@@ -0,0 +1 @@

+2 -1
View File
@@ -1,7 +1,7 @@
package version package version
var ( var (
Version = "1.0.11" Version = "1.0.12"
Repo = "MengMengCode/CLICD" Repo = "MengMengCode/CLICD"
) )
@@ -17,3 +17,4 @@ func Current() string {
+4 -6
View File
@@ -16,8 +16,8 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
const radius = (size - strokeWidth) / 2 const radius = (size - strokeWidth) / 2
const circumference = radius * 2 * Math.PI const circumference = radius * 2 * Math.PI
const percentage = Math.min(Math.max(value / max * 100, 0), 100) const percentage = max === Infinity ? Math.max(value, 0) : Math.min(Math.max(value / max * 100, 0), 100)
const strokeDashoffset = circumference - (percentage / 100) * circumference const strokeDashoffset = circumference - (Math.min(percentage, 100) / 100) * circumference
const bgStroke = isDark ? '#374151' : '#f3f4f6' const bgStroke = isDark ? '#374151' : '#f3f4f6'
const progressStroke = isDark ? '#f9fafb' : '#000000' const progressStroke = isDark ? '#f9fafb' : '#000000'
@@ -51,7 +51,7 @@ export function RingStat({ value, max = 100, label, subLabel, size = 120, stroke
</svg> </svg>
{/* Center value */} {/* Center value */}
<div className="absolute inset-0 flex flex-col items-center justify-center"> <div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-2xl font-bold text-black dark:text-white">{value.toFixed(percentage < 1 ? 2 : 1)}%</span> <span className="text-2xl font-bold text-black dark:text-white">{percentage.toFixed(percentage < 1 ? 2 : 1)}%</span>
</div> </div>
</div> </div>
<div className="mt-2 text-center"> <div className="mt-2 text-center">
@@ -73,7 +73,6 @@ interface RingStatsProps {
swapUsed?: number swapUsed?: number
swapTotal?: number swapTotal?: number
loadPercent: number loadPercent: number
loadStatus: string
diskPercent: number diskPercent: number
diskUsed: number diskUsed: number
diskTotal: number diskTotal: number
@@ -90,7 +89,6 @@ export default function RingStats({
swapUsed = 0, swapUsed = 0,
swapTotal = 0, swapTotal = 0,
loadPercent, loadPercent,
loadStatus,
diskPercent, diskPercent,
diskUsed, diskUsed,
diskTotal, diskTotal,
@@ -125,8 +123,8 @@ export default function RingStats({
)} )}
<RingStat <RingStat
value={loadPercent} value={loadPercent}
max={Infinity}
label="负载" label="负载"
subLabel={loadStatus}
/> />
<RingStat <RingStat
value={diskPercent} value={diskPercent}
+3 -2
View File
@@ -654,6 +654,7 @@ export default function ContainerDetail() {
const filtered = filterHistory(history, range) const filtered = filterHistory(history, range)
const cpuPct = clamp(usage?.cpu_usage_pct || 0) const cpuPct = clamp(usage?.cpu_usage_pct || 0)
const ramPct = container.ram_mb > 0 ? clamp(((usage?.memory_usage_bytes || 0) / (container.ram_mb * 1024 * 1024)) * 100) : 0 const ramPct = container.ram_mb > 0 ? clamp(((usage?.memory_usage_bytes || 0) / (container.ram_mb * 1024 * 1024)) * 100) : 0
const loadPct = container.vcpu > 0 ? ((usage?.load1 || 0) / container.vcpu) * 100 : 0
const diskPct = container.disk_gb > 0 ? clamp(((usage?.disk_usage_bytes || 0) / (container.disk_gb * 1024 * 1024 * 1024)) * 100) : 0 const diskPct = container.disk_gb > 0 ? clamp(((usage?.disk_usage_bytes || 0) / (container.disk_gb * 1024 * 1024 * 1024)) * 100) : 0
const networkBps = (usage?.network_rx_bps || 0) + (usage?.network_tx_bps || 0) const networkBps = (usage?.network_rx_bps || 0) + (usage?.network_tx_bps || 0)
const rx = usage?.network_rx_bps || 0 const rx = usage?.network_rx_bps || 0
@@ -873,9 +874,9 @@ export default function ContainerDetail() {
subLabel={`${formatMB(usage?.memory_usage_bytes || 0)} / ${formatMB(container.ram_mb * 1024 * 1024)}`} subLabel={`${formatMB(usage?.memory_usage_bytes || 0)} / ${formatMB(container.ram_mb * 1024 * 1024)}`}
/> />
<RingStat <RingStat
value={Math.min(cpuPct, 100)} value={loadPct}
max={Infinity}
label="负载" label="负载"
subLabel={cpuPct < 70 ? '正常' : cpuPct < 90 ? '中等' : '高'}
/> />
<RingStat <RingStat
value={diskPct} value={diskPct}
+1 -2
View File
@@ -119,8 +119,7 @@ export default function Dashboard() {
ramPercent={host.ram.total_mb > 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0} ramPercent={host.ram.total_mb > 0 ? (host.ram.used_mb / host.ram.total_mb) * 100 : 0}
ramUsed={host.ram.used_mb} ramUsed={host.ram.used_mb}
ramTotal={host.ram.total_mb} ramTotal={host.ram.total_mb}
loadPercent={Math.min((host.load.load1 / host.cpu.cores) * 100, 100)} loadPercent={(host.load.load1 / Math.max(host.cpu.cores, 1)) * 100}
loadStatus={host.load.load1 < host.cpu.cores * 0.7 ? '正常' : host.load.load1 < host.cpu.cores * 1.0 ? '中等' : '高'}
diskPercent={host.disk.total_gb > 0 ? (host.disk.used_gb / host.disk.total_gb) * 100 : 0} diskPercent={host.disk.total_gb > 0 ? (host.disk.used_gb / host.disk.total_gb) * 100 : 0}
diskUsed={host.disk.used_gb * 1024} diskUsed={host.disk.used_gb * 1024}
diskTotal={host.disk.total_gb * 1024} diskTotal={host.disk.total_gb * 1024}
+23 -5
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { Network, RefreshCw, Route, Search, Server, X } from 'lucide-react' import { RefreshCw, Search, Server, X } from 'lucide-react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { getRoutingInfo, RoutingInfo, NAT4Route, IPv6Route } from '../services/api' import { getRoutingInfo, RoutingInfo, NAT4Route, IPv6Route } from '../services/api'
@@ -96,7 +96,7 @@ export default function Routing() {
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<CapacityCard <CapacityCard
title="NAT4 端口" title="NAT4 端口"
icon={<Route className="h-5 w-5 text-gray-600" />} icon={<Nat4Icon />}
remaining={routing?.nat4.remaining || '0'} remaining={routing?.nat4.remaining || '0'}
total={routing?.nat4.total || '0'} total={routing?.nat4.total || '0'}
used={routing?.nat4.used || 0} used={routing?.nat4.used || 0}
@@ -104,7 +104,7 @@ export default function Routing() {
/> />
<CapacityCard <CapacityCard
title="IPv6 地址" title="IPv6 地址"
icon={<Network className="h-5 w-5 text-gray-600" />} icon={<IPv6Icon />}
remaining={formatCapacity(routing?.ipv6.remaining || '0')} remaining={formatCapacity(routing?.ipv6.remaining || '0')}
total={formatCapacity(routing?.ipv6.total || '0')} total={formatCapacity(routing?.ipv6.total || '0')}
used={routing?.ipv6.used || 0} used={routing?.ipv6.used || 0}
@@ -137,7 +137,7 @@ export default function Routing() {
</div> </div>
</div> </div>
{nat4Mappings.length === 0 ? ( {nat4Mappings.length === 0 ? (
<EmptyState icon={<Route className="h-7 w-7 text-gray-400" />} text="暂无 NAT4 端口映射" /> <EmptyState icon={<Nat4Icon className="h-7 w-7" />} text="暂无 NAT4 端口映射" />
) : ( ) : (
<> <>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -214,7 +214,7 @@ export default function Routing() {
</div> </div>
</div> </div>
{ipv6Assignments.length === 0 ? ( {ipv6Assignments.length === 0 ? (
<EmptyState icon={<Network className="h-7 w-7 text-gray-400" />} text="暂无 IPv6 地址分配" /> <EmptyState icon={<IPv6Icon className="h-7 w-7" />} text="暂无 IPv6 地址分配" />
) : ( ) : (
<> <>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -357,3 +357,21 @@ function formatCapacity(value: string): string {
if (value === 'large') return '充足' if (value === 'large') return '充足'
return value return value
} }
function Nat4Icon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M797.866667 128c64 0 115.2 51.2 119.466666 110.933333v558.933334c0 64-51.2 115.2-110.933333 119.466666H243.2c-59.733333 0-110.933333-51.2-115.2-110.933333V247.466667C128 187.733333 174.933333 136.533333 234.666667 128h563.2z m38.4 473.6H204.8v196.266667c0 21.333333 17.066667 38.4 38.4 38.4h554.666667c21.333333 0 38.4-17.066667 38.4-38.4v-196.266667z m-315.733334 76.8c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4H320c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133333-38.4h204.8z m157.866667 0c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4h-46.933334c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133334-38.4h46.933333z m119.466667-473.6h-554.666667c-21.333333 0-38.4 17.066667-38.4 38.4v277.333333h631.466667V243.2c0-17.066667-17.066667-34.133333-38.4-38.4z" />
<path d="M277.333333 426.666667V243.2h34.133334V426.666667h-34.133334zM426.666667 358.4h-34.133334V426.666667h-34.133333V243.2h72.533333c38.4 0 59.733333 25.6 59.733334 55.466667s-25.6 59.733333-64 59.733333z m-4.266667-81.066667h-34.133333v51.2h34.133333c17.066667 0 25.6-8.533333 25.6-25.6s-8.533333-25.6-25.6-25.6zM571.733333 426.666667h-25.6l-51.2-132.266667h34.133334l25.6 81.066667 25.6-81.066667h34.133333l-42.666667 132.266667zM733.866667 401.066667v25.6h-34.133334v-25.6h-72.533333v-29.866667l64-123.733333h38.4l-64 123.733333h38.4v-34.133333h34.133333v34.133333h17.066667v29.866667h-21.333333z" />
</svg>
)
}
function IPv6Icon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M797.866667 128c64 0 115.2 51.2 119.466666 110.933333v558.933334c0 64-51.2 115.2-110.933333 119.466666H243.2c-59.733333 0-110.933333-51.2-115.2-110.933333V247.466667C128 187.733333 174.933333 136.533333 234.666667 128h563.2z m38.4 473.6H204.8v196.266667c0 21.333333 17.066667 38.4 38.4 38.4h554.666667c21.333333 0 38.4-17.066667 38.4-38.4v-196.266667z m-315.733334 76.8c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4H320c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133333-38.4h204.8z m157.866667 0c21.333333 0 38.4 17.066667 38.4 42.666667 0 17.066667-12.8 34.133333-34.133333 38.4h-46.933334c-21.333333 0-38.4-17.066667-38.4-42.666667 0-17.066667 12.8-34.133333 34.133334-38.4h46.933333z m119.466667-473.6h-554.666667c-21.333333 0-38.4 17.066667-38.4 38.4v277.333333h631.466667V243.2c0-17.066667-17.066667-34.133333-38.4-38.4z" />
<path d="M277.333333 426.666667V243.2h34.133334V426.666667h-34.133334zM426.666667 358.4h-34.133334V426.666667h-34.133333V243.2h72.533333c38.4 0 59.733333 25.6 59.733334 55.466667s-25.6 59.733333-64 59.733333z m-4.266667-81.066667h-34.133333v51.2h34.133333c17.066667 0 25.6-8.533333 25.6-25.6s-8.533333-25.6-25.6-25.6zM571.733333 426.666667h-25.6l-51.2-132.266667h34.133334l25.6 81.066667 25.6-81.066667h34.133333l-42.666667 132.266667zM691.2 426.666667c-34.133333 0-55.466667-21.333333-55.466667-55.466667 0-17.066667 8.533333-34.133333 17.066667-46.933333l38.4-76.8h38.4l-38.4 76.8c4.266667 0 8.533333-4.266667 12.8-4.266667 25.6 0 46.933333 21.333333 46.933333 55.466667-4.266667 29.866667-29.866667 51.2-59.733333 51.2z m0-81.066667c-12.8 0-25.6 8.533333-25.6 25.6 0 17.066667 8.533333 25.6 25.6 25.6s25.6-8.533333 25.6-25.6c-4.266667-17.066667-12.8-25.6-25.6-25.6z" />
</svg>
)
}
+3
View File
@@ -170,6 +170,9 @@ export interface ContainerUsage {
disk_write_bytes: number disk_write_bytes: number
disk_read_bps: number disk_read_bps: number
disk_write_bps: number disk_write_bps: number
load1: number
load5: number
load15: number
} }
export interface APIResponse<T = unknown> { export interface APIResponse<T = unknown> {