mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-06 05:52:19 +08:00
Support Debian 13.
#29 and fix useage chart data display problem, storage on database instead of localstorage.
This commit is contained in:
@@ -71,3 +71,4 @@ deploy.ps1
|
|||||||
backend/clicd
|
backend/clicd
|
||||||
api.md
|
api.md
|
||||||
deploy-arm.ps1
|
deploy-arm.ps1
|
||||||
|
deploy-dhcp.ps1
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"clicd/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ContainerMetricPoint struct {
|
||||||
|
TS int64 `json:"ts"`
|
||||||
|
CPU float64 `json:"cpu"`
|
||||||
|
Memory float64 `json:"memory"`
|
||||||
|
Network float64 `json:"network"`
|
||||||
|
NetworkRx float64 `json:"network_rx"`
|
||||||
|
NetworkTx float64 `json:"network_tx"`
|
||||||
|
DiskIO float64 `json:"disk_io"`
|
||||||
|
DiskRead float64 `json:"disk_read"`
|
||||||
|
DiskWrite float64 `json:"disk_write"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var containerMetricSamplerOnce sync.Once
|
||||||
|
var containerMetricMu sync.RWMutex
|
||||||
|
var containerMetricHistory = map[string][]ContainerMetricPoint{}
|
||||||
|
var containerMetricInFlight sync.Map
|
||||||
|
|
||||||
|
const (
|
||||||
|
containerMetricSampleInterval = 30 * time.Second
|
||||||
|
containerMetricSampleTimeout = 20 * time.Second
|
||||||
|
containerMetricConcurrency = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartContainerMetricSampler() {
|
||||||
|
containerMetricSamplerOnce.Do(func() {
|
||||||
|
go func() {
|
||||||
|
sampleAllContainerMetrics()
|
||||||
|
ticker := time.NewTicker(containerMetricSampleInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
sampleAllContainerMetrics()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleAllContainerMetrics() {
|
||||||
|
containers, _ := listByRuntime()
|
||||||
|
sem := make(chan struct{}, containerMetricConcurrency)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, c := range containers {
|
||||||
|
c := c
|
||||||
|
if c.Status != "running" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sem <- struct{}{}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-sem }()
|
||||||
|
sampleContainerMetricWithTimeout(c)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
pruneContainerMetricHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleContainerMetricWithTimeout(c config.Container) {
|
||||||
|
key := containerMetricKey(c)
|
||||||
|
if key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, loaded := containerMetricInFlight.LoadOrStore(key, struct{}{}); loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
defer containerMetricInFlight.Delete(key)
|
||||||
|
if usage, err := usageByRuntime(c.ID); err == nil {
|
||||||
|
appendContainerMetricPoint(c, usage)
|
||||||
|
}
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(containerMetricSampleTimeout):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendContainerMetricPoint(c config.Container, usage map[string]interface{}) {
|
||||||
|
key := containerMetricKey(c)
|
||||||
|
if key == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
memoryTotal := numberFromUsage(usage, "memory_total_bytes")
|
||||||
|
if memoryTotal <= 0 {
|
||||||
|
memoryTotal = float64(c.RAMMB) * 1024 * 1024
|
||||||
|
}
|
||||||
|
memoryPct := 0.0
|
||||||
|
if memoryTotal > 0 {
|
||||||
|
memoryPct = clampPercent(numberFromUsage(usage, "memory_usage_bytes") / memoryTotal * 100)
|
||||||
|
}
|
||||||
|
vcpu := c.VCPU
|
||||||
|
if vcpu <= 0 {
|
||||||
|
vcpu = 1
|
||||||
|
}
|
||||||
|
cpuPct := clampPercent(numberFromUsage(usage, "cpu_usage_pct") / vcpu)
|
||||||
|
networkRx := positiveNumberFromUsage(usage, "network_rx_bps")
|
||||||
|
networkTx := positiveNumberFromUsage(usage, "network_tx_bps")
|
||||||
|
diskRead := positiveNumberFromUsage(usage, "disk_read_bps")
|
||||||
|
diskWrite := positiveNumberFromUsage(usage, "disk_write_bps")
|
||||||
|
point := ContainerMetricPoint{
|
||||||
|
TS: time.Now().UnixMilli(),
|
||||||
|
CPU: cpuPct,
|
||||||
|
Memory: memoryPct,
|
||||||
|
NetworkRx: networkRx,
|
||||||
|
NetworkTx: networkTx,
|
||||||
|
Network: networkRx + networkTx,
|
||||||
|
DiskRead: diskRead,
|
||||||
|
DiskWrite: diskWrite,
|
||||||
|
DiskIO: diskRead + diskWrite,
|
||||||
|
}
|
||||||
|
cutoff := time.Now().Add(-hostMetricRetention).UnixMilli()
|
||||||
|
|
||||||
|
containerMetricMu.Lock()
|
||||||
|
defer containerMetricMu.Unlock()
|
||||||
|
|
||||||
|
history := containerMetricHistory[key]
|
||||||
|
keepFrom := 0
|
||||||
|
for keepFrom < len(history) && history[keepFrom].TS < cutoff {
|
||||||
|
keepFrom++
|
||||||
|
}
|
||||||
|
if keepFrom > 0 {
|
||||||
|
copy(history, history[keepFrom:])
|
||||||
|
history = history[:len(history)-keepFrom]
|
||||||
|
}
|
||||||
|
containerMetricHistory[key] = append(history, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContainerMetricHistory(c *config.Container) []ContainerMetricPoint {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
key := containerMetricKey(*c)
|
||||||
|
containerMetricMu.RLock()
|
||||||
|
defer containerMetricMu.RUnlock()
|
||||||
|
|
||||||
|
history := containerMetricHistory[key]
|
||||||
|
result := make([]ContainerMetricPoint, len(history))
|
||||||
|
copy(result, history)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func pruneContainerMetricHistory() {
|
||||||
|
cutoff := time.Now().Add(-hostMetricRetention).UnixMilli()
|
||||||
|
valid := map[string]bool{}
|
||||||
|
if config.AppConfig != nil {
|
||||||
|
for _, c := range config.AppConfig.Containers {
|
||||||
|
valid[containerMetricKey(c)] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
containerMetricMu.Lock()
|
||||||
|
defer containerMetricMu.Unlock()
|
||||||
|
|
||||||
|
for key, history := range containerMetricHistory {
|
||||||
|
if !valid[key] {
|
||||||
|
delete(containerMetricHistory, key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keepFrom := 0
|
||||||
|
for keepFrom < len(history) && history[keepFrom].TS < cutoff {
|
||||||
|
keepFrom++
|
||||||
|
}
|
||||||
|
if keepFrom > 0 {
|
||||||
|
copy(history, history[keepFrom:])
|
||||||
|
containerMetricHistory[key] = history[:len(history)-keepFrom]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerMetricKey(c config.Container) string {
|
||||||
|
if c.UUID != "" {
|
||||||
|
return "uuid:" + c.UUID
|
||||||
|
}
|
||||||
|
if c.ID > 0 {
|
||||||
|
return fmt.Sprintf("id:%d", c.ID)
|
||||||
|
}
|
||||||
|
if c.Name != "" {
|
||||||
|
return "name:" + c.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func numberFromUsage(usage map[string]interface{}, key string) float64 {
|
||||||
|
value, ok := usage[key]
|
||||||
|
if !ok || value == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
case float32:
|
||||||
|
return float64(v)
|
||||||
|
case int:
|
||||||
|
return float64(v)
|
||||||
|
case int64:
|
||||||
|
return float64(v)
|
||||||
|
case int32:
|
||||||
|
return float64(v)
|
||||||
|
case uint:
|
||||||
|
return float64(v)
|
||||||
|
case uint64:
|
||||||
|
return float64(v)
|
||||||
|
case uint32:
|
||||||
|
return float64(v)
|
||||||
|
case json.Number:
|
||||||
|
n, _ := v.Float64()
|
||||||
|
return n
|
||||||
|
case string:
|
||||||
|
n, _ := strconv.ParseFloat(v, 64)
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveNumberFromUsage(usage map[string]interface{}, key string) float64 {
|
||||||
|
value := numberFromUsage(usage, key)
|
||||||
|
if value < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -132,6 +132,11 @@ func HandleSingleContainer(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
getUsage(w, r, id)
|
getUsage(w, r, id)
|
||||||
|
case action == "history" && r.Method == http.MethodGet:
|
||||||
|
if !requireScope(w, r, "container:read") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: getContainerMetricHistory(c)})
|
||||||
case action == "traffic" && r.Method == http.MethodGet:
|
case action == "traffic" && r.Method == http.MethodGet:
|
||||||
if !requireScope(w, r, "container:read") {
|
if !requireScope(w, r, "container:read") {
|
||||||
return
|
return
|
||||||
@@ -661,6 +666,18 @@ func HandleHostInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: info})
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: info})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleHostHistory returns host resource samples collected by the server.
|
||||||
|
func HandleHostHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
jsonResponse(w, http.StatusMethodNotAllowed, APIResponse{Success: false, Message: "Method not allowed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !requireScope(w, r, "host:read") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonResponse(w, http.StatusOK, APIResponse{Success: true, Data: getHostMetricHistory()})
|
||||||
|
}
|
||||||
|
|
||||||
func resetSSHPassword(w http.ResponseWriter, r *http.Request, id int) {
|
func resetSSHPassword(w http.ResponseWriter, r *http.Request, id int) {
|
||||||
c := config.FindContainer(id)
|
c := config.FindContainer(id)
|
||||||
if c != nil && lxc.IsExpired(*c) {
|
if c != nil && lxc.IsExpired(*c) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -217,14 +218,35 @@ type DiskIOInfo struct {
|
|||||||
WriteBps float64 `json:"write_bps"`
|
WriteBps float64 `json:"write_bps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HostMetricPoint struct {
|
||||||
|
TS int64 `json:"ts"`
|
||||||
|
CPU float64 `json:"cpu"`
|
||||||
|
Memory float64 `json:"memory"`
|
||||||
|
Network float64 `json:"network"`
|
||||||
|
NetworkRx float64 `json:"network_rx"`
|
||||||
|
NetworkTx float64 `json:"network_tx"`
|
||||||
|
DiskIO float64 `json:"disk_io"`
|
||||||
|
DiskRead float64 `json:"disk_read"`
|
||||||
|
DiskWrite float64 `json:"disk_write"`
|
||||||
|
DiskUsagePct float64 `json:"disk_usage_pct"`
|
||||||
|
}
|
||||||
|
|
||||||
var hostCPUMu sync.Mutex
|
var hostCPUMu sync.Mutex
|
||||||
var lastHostCPU cpuTimes
|
var lastHostCPU cpuTimes
|
||||||
var hostIOMu sync.Mutex
|
var hostIOMu sync.Mutex
|
||||||
var lastHostIO hostIOSample
|
var lastHostIO hostIOSample
|
||||||
|
var hostMetricSamplerOnce sync.Once
|
||||||
|
var hostMetricMu sync.RWMutex
|
||||||
|
var hostMetricHistory []HostMetricPoint
|
||||||
var egressIPv4Mu sync.Mutex
|
var egressIPv4Mu sync.Mutex
|
||||||
var cachedEgressIPv4 lxc.PublicIPInfo
|
var cachedEgressIPv4 lxc.PublicIPInfo
|
||||||
var cachedEgressIPv4At time.Time
|
var cachedEgressIPv4At time.Time
|
||||||
|
|
||||||
|
const (
|
||||||
|
hostMetricSampleInterval = 30 * time.Second
|
||||||
|
hostMetricRetention = 7 * 24 * time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
type cpuTimes struct {
|
type cpuTimes struct {
|
||||||
Total uint64
|
Total uint64
|
||||||
Idle uint64
|
Idle uint64
|
||||||
@@ -239,6 +261,10 @@ type hostIOSample struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getHostInfo() HostInfo {
|
func getHostInfo() HostInfo {
|
||||||
|
return getHostInfoWithNetworkDetails(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostInfoWithNetworkDetails(includeDetails bool) HostInfo {
|
||||||
info := HostInfo{
|
info := HostInfo{
|
||||||
CPU: CpuInfo{Cores: runtime.NumCPU()},
|
CPU: CpuInfo{Cores: runtime.NumCPU()},
|
||||||
}
|
}
|
||||||
@@ -246,7 +272,7 @@ func getHostInfo() HostInfo {
|
|||||||
info.RAM = getMemoryInfo()
|
info.RAM = getMemoryInfo()
|
||||||
info.Disk = getDiskInfo()
|
info.Disk = getDiskInfo()
|
||||||
info.CPU.Usage = getCPUUsage()
|
info.CPU.Usage = getCPUUsage()
|
||||||
info.Network, info.DiskIO = getHostRates()
|
info.Network, info.DiskIO = getHostRates(includeDetails)
|
||||||
info.Load = getLoadInfo()
|
info.Load = getLoadInfo()
|
||||||
info.Runtime = detectRuntimeProbeQuick()
|
info.Runtime = detectRuntimeProbeQuick()
|
||||||
return info
|
return info
|
||||||
@@ -274,6 +300,79 @@ func detectRuntimeProbeQuick() HostRuntimeProbe {
|
|||||||
return probe
|
return probe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StartHostMetricSampler() {
|
||||||
|
hostMetricSamplerOnce.Do(func() {
|
||||||
|
appendHostMetricPoint(getHostInfoWithNetworkDetails(false))
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(hostMetricSampleInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
appendHostMetricPoint(getHostInfoWithNetworkDetails(false))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendHostMetricPoint(info HostInfo) {
|
||||||
|
memoryPct := 0.0
|
||||||
|
if info.RAM.TotalMB > 0 {
|
||||||
|
memoryPct = clampPercent(float64(info.RAM.UsedMB) / float64(info.RAM.TotalMB) * 100)
|
||||||
|
}
|
||||||
|
diskUsagePct := 0.0
|
||||||
|
if info.Disk.TotalGB > 0 {
|
||||||
|
diskUsagePct = clampPercent(info.Disk.UsedGB / info.Disk.TotalGB * 100)
|
||||||
|
}
|
||||||
|
point := HostMetricPoint{
|
||||||
|
TS: time.Now().UnixMilli(),
|
||||||
|
CPU: clampPercent(info.CPU.Usage),
|
||||||
|
Memory: memoryPct,
|
||||||
|
NetworkRx: info.Network.RXBps,
|
||||||
|
NetworkTx: info.Network.TXBps,
|
||||||
|
Network: info.Network.RXBps + info.Network.TXBps,
|
||||||
|
DiskRead: info.DiskIO.ReadBps,
|
||||||
|
DiskWrite: info.DiskIO.WriteBps,
|
||||||
|
DiskIO: info.DiskIO.ReadBps + info.DiskIO.WriteBps,
|
||||||
|
DiskUsagePct: diskUsagePct,
|
||||||
|
}
|
||||||
|
cutoff := time.Now().Add(-hostMetricRetention).UnixMilli()
|
||||||
|
|
||||||
|
hostMetricMu.Lock()
|
||||||
|
defer hostMetricMu.Unlock()
|
||||||
|
|
||||||
|
keepFrom := 0
|
||||||
|
for keepFrom < len(hostMetricHistory) && hostMetricHistory[keepFrom].TS < cutoff {
|
||||||
|
keepFrom++
|
||||||
|
}
|
||||||
|
if keepFrom > 0 {
|
||||||
|
copy(hostMetricHistory, hostMetricHistory[keepFrom:])
|
||||||
|
hostMetricHistory = hostMetricHistory[:len(hostMetricHistory)-keepFrom]
|
||||||
|
}
|
||||||
|
hostMetricHistory = append(hostMetricHistory, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostMetricHistory() []HostMetricPoint {
|
||||||
|
hostMetricMu.RLock()
|
||||||
|
defer hostMetricMu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]HostMetricPoint, len(hostMetricHistory))
|
||||||
|
copy(result, hostMetricHistory)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampPercent(value float64) float64 {
|
||||||
|
if value < 0 || !isFiniteFloat(value) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if value > 100 {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFiniteFloat(value float64) bool {
|
||||||
|
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||||
|
}
|
||||||
|
|
||||||
func getMemoryInfo() MemoryInfo {
|
func getMemoryInfo() MemoryInfo {
|
||||||
f, err := os.Open("/proc/meminfo")
|
f, err := os.Open("/proc/meminfo")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -419,12 +518,13 @@ func parseSizeGBf(s string) (float64, error) {
|
|||||||
return val, err
|
return val, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHostRates() (NetworkInfo, DiskIOInfo) {
|
func getHostRates(includeDetails bool) (NetworkInfo, DiskIOInfo) {
|
||||||
rx, tx := readHostNetworkBytes()
|
rx, tx := readHostNetworkBytes()
|
||||||
readBytes, writeBytes := readHostDiskBytes()
|
readBytes, writeBytes := readHostDiskBytes()
|
||||||
now := unixNano()
|
now := unixNano()
|
||||||
|
|
||||||
network := NetworkInfo{RXBytes: rx, TXBytes: tx}
|
network := NetworkInfo{RXBytes: rx, TXBytes: tx}
|
||||||
|
if includeDetails {
|
||||||
publicIPv4 := detectDisplayPublicIPv4()
|
publicIPv4 := detectDisplayPublicIPv4()
|
||||||
network.PublicIPv4 = publicIPv4.Address
|
network.PublicIPv4 = publicIPv4.Address
|
||||||
network.PublicIPv4Interface = publicIPv4.Interface
|
network.PublicIPv4Interface = publicIPv4.Interface
|
||||||
@@ -434,6 +534,7 @@ func getHostRates() (NetworkInfo, DiskIOInfo) {
|
|||||||
network.PublicIPv6 = network.IPv6Prefixes[0].Address
|
network.PublicIPv6 = network.IPv6Prefixes[0].Address
|
||||||
network.PublicIPv6Interface = network.IPv6Prefixes[0].Interface
|
network.PublicIPv6Interface = network.IPv6Prefixes[0].Interface
|
||||||
}
|
}
|
||||||
|
}
|
||||||
diskIO := DiskIOInfo{ReadBytes: readBytes, WriteBytes: writeBytes}
|
diskIO := DiskIOInfo{ReadBytes: readBytes, WriteBytes: writeBytes}
|
||||||
|
|
||||||
hostIOMu.Lock()
|
hostIOMu.Lock()
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ func isSubUserBlockedAction(action string, method string) bool {
|
|||||||
return method != http.MethodGet
|
return method != http.MethodGet
|
||||||
}
|
}
|
||||||
switch action {
|
switch action {
|
||||||
case "usage", "traffic":
|
case "usage", "traffic", "history":
|
||||||
return method != http.MethodGet
|
return method != http.MethodGet
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
@@ -665,7 +665,7 @@ func isSubUserContainerActionAllowed(action string, method string) bool {
|
|||||||
return method == http.MethodGet
|
return method == http.MethodGet
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case action == "usage" || action == "traffic" || action == "random-port":
|
case action == "usage" || action == "traffic" || action == "history" || action == "random-port":
|
||||||
return method == http.MethodGet
|
return method == http.MethodGet
|
||||||
case action == "snapshots":
|
case action == "snapshots":
|
||||||
return method == http.MethodGet || method == http.MethodPost
|
return method == http.MethodGet || method == http.MethodPost
|
||||||
|
|||||||
@@ -46,12 +46,25 @@ func amd64Images() []Image {
|
|||||||
Description: "Ubuntu 22.04 LTS cloud image for KVM",
|
Description: "Ubuntu 22.04 LTS cloud image for KVM",
|
||||||
URL: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img",
|
URL: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "kvm-debian-trixie", Name: "Debian 13 KVM",
|
||||||
|
Distro: "debian", Release: "trixie", Arch: "amd64",
|
||||||
|
Description: "Debian 13 generic cloud image for KVM",
|
||||||
|
URL: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "kvm-debian-bookworm", Name: "Debian 12 KVM",
|
ID: "kvm-debian-bookworm", Name: "Debian 12 KVM",
|
||||||
Distro: "debian", Release: "bookworm", Arch: "amd64",
|
Distro: "debian", Release: "bookworm", Arch: "amd64",
|
||||||
Description: "Debian 12 generic cloud image for KVM",
|
Description: "Debian 12 generic cloud image for KVM",
|
||||||
URL: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2",
|
URL: "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "kvm-debian-trixie-xfce", Name: "Debian 13 XFCE KVM",
|
||||||
|
Distro: "debian", Release: "trixie", Arch: "amd64",
|
||||||
|
Description: "Debian 13 generic cloud image with XFCE desktop provisioned via cloud-init",
|
||||||
|
URL: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2",
|
||||||
|
Desktop: "xfce",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "kvm-debian-bookworm-xfce", Name: "Debian 12 XFCE KVM",
|
ID: "kvm-debian-bookworm-xfce", Name: "Debian 12 XFCE KVM",
|
||||||
Distro: "debian", Release: "bookworm", Arch: "amd64",
|
Distro: "debian", Release: "bookworm", Arch: "amd64",
|
||||||
@@ -118,6 +131,12 @@ func arm64Images() []Image {
|
|||||||
Description: "Ubuntu 22.04 LTS cloud image for ARM64 KVM",
|
Description: "Ubuntu 22.04 LTS cloud image for ARM64 KVM",
|
||||||
URL: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img",
|
URL: "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "kvm-debian-trixie", Name: "Debian 13 KVM",
|
||||||
|
Distro: "debian", Release: "trixie", Arch: "arm64",
|
||||||
|
Description: "Debian 13 generic cloud image for ARM64 KVM",
|
||||||
|
URL: "https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-arm64.qcow2",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "kvm-debian-bookworm", Name: "Debian 12 KVM",
|
ID: "kvm-debian-bookworm", Name: "Debian 12 KVM",
|
||||||
Distro: "debian", Release: "bookworm", Arch: "arm64",
|
Distro: "debian", Release: "bookworm", Arch: "arm64",
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ func GetTemplates() []Template {
|
|||||||
Distro: "ubuntu", Release: "jammy", Arch: arch,
|
Distro: "ubuntu", Release: "jammy", Arch: arch,
|
||||||
Description: "Ubuntu 22.04 LTS",
|
Description: "Ubuntu 22.04 LTS",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "debian-trixie", Name: "Debian 13",
|
||||||
|
Distro: "debian", Release: "trixie", Arch: arch,
|
||||||
|
Description: "Debian 13 (Trixie)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ID: "debian-bookworm", Name: "Debian 12",
|
ID: "debian-bookworm", Name: "Debian 12",
|
||||||
Distro: "debian", Release: "bookworm", Arch: arch,
|
Distro: "debian", Release: "bookworm", Arch: arch,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ func setupRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/images/enabled", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleEnabledImages))))
|
mux.HandleFunc("/api/images/enabled", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleEnabledImages))))
|
||||||
mux.HandleFunc("/api/dashboard", corsMiddleware(api.AdminMiddleware(api.HandleDashboard)))
|
mux.HandleFunc("/api/dashboard", corsMiddleware(api.AdminMiddleware(api.HandleDashboard)))
|
||||||
mux.HandleFunc("/api/host-info", corsMiddleware(api.AdminMiddleware(api.HandleHostInfo)))
|
mux.HandleFunc("/api/host-info", corsMiddleware(api.AdminMiddleware(api.HandleHostInfo)))
|
||||||
|
mux.HandleFunc("/api/host-history", corsMiddleware(api.AdminMiddleware(api.HandleHostHistory)))
|
||||||
mux.HandleFunc("/api/host-report", corsMiddleware(api.AdminMiddleware(api.HandleHostReport)))
|
mux.HandleFunc("/api/host-report", corsMiddleware(api.AdminMiddleware(api.HandleHostReport)))
|
||||||
mux.HandleFunc("/api/snapshots", corsMiddleware(api.AdminMiddleware(api.HandleSnapshots)))
|
mux.HandleFunc("/api/snapshots", corsMiddleware(api.AdminMiddleware(api.HandleSnapshots)))
|
||||||
mux.HandleFunc("/api/routing/ipv4-scan", corsMiddleware(api.AdminMiddleware(api.HandleRoutingIPv4Scan)))
|
mux.HandleFunc("/api/routing/ipv4-scan", corsMiddleware(api.AdminMiddleware(api.HandleRoutingIPv4Scan)))
|
||||||
@@ -104,6 +105,7 @@ func setupRoutes(mux *http.ServeMux) {
|
|||||||
mux.HandleFunc("/api/v1/images/toggle", corsMiddleware(api.AuthMiddleware(api.HandleImageToggle)))
|
mux.HandleFunc("/api/v1/images/toggle", corsMiddleware(api.AuthMiddleware(api.HandleImageToggle)))
|
||||||
mux.HandleFunc("/api/v1/images/enabled", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleEnabledImages))))
|
mux.HandleFunc("/api/v1/images/enabled", corsMiddleware(api.AuthMiddleware(api.SubUserMiddleware(api.HandleEnabledImages))))
|
||||||
mux.HandleFunc("/api/v1/host-info", corsMiddleware(api.AuthMiddleware(api.HandleHostInfo)))
|
mux.HandleFunc("/api/v1/host-info", corsMiddleware(api.AuthMiddleware(api.HandleHostInfo)))
|
||||||
|
mux.HandleFunc("/api/v1/host-history", corsMiddleware(api.AuthMiddleware(api.HandleHostHistory)))
|
||||||
mux.HandleFunc("/api/v1/host-report", corsMiddleware(api.AuthMiddleware(api.HandleHostReport)))
|
mux.HandleFunc("/api/v1/host-report", corsMiddleware(api.AuthMiddleware(api.HandleHostReport)))
|
||||||
mux.HandleFunc("/api/v1/snapshots", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("snapshot:read", api.HandleSnapshots))))
|
mux.HandleFunc("/api/v1/snapshots", corsMiddleware(api.AuthMiddleware(api.ScopeMiddleware("snapshot:read", api.HandleSnapshots))))
|
||||||
mux.HandleFunc("/api/v1/routing/ipv4-scan", corsMiddleware(api.AuthMiddleware(api.HandleRoutingIPv4Scan)))
|
mux.HandleFunc("/api/v1/routing/ipv4-scan", corsMiddleware(api.AuthMiddleware(api.HandleRoutingIPv4Scan)))
|
||||||
@@ -174,6 +176,8 @@ func setupRoutes(mux *http.ServeMux) {
|
|||||||
func Run() error {
|
func Run() error {
|
||||||
// Use embedded frontend files
|
// Use embedded frontend files
|
||||||
webFS = GetEmbeddedFS()
|
webFS = GetEmbeddedFS()
|
||||||
|
api.StartHostMetricSampler()
|
||||||
|
api.StartContainerMetricSampler()
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
setupRoutes(mux)
|
setupRoutes(mux)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
assignIPv6,
|
assignIPv6,
|
||||||
APIResponse,
|
APIResponse,
|
||||||
Container,
|
Container,
|
||||||
|
ContainerMetricPoint as ContainerMetricSample,
|
||||||
ContainerUsage,
|
ContainerUsage,
|
||||||
createSubUser,
|
createSubUser,
|
||||||
createContainerSnapshot,
|
createContainerSnapshot,
|
||||||
@@ -39,6 +40,7 @@ import {
|
|||||||
deleteContainerSnapshot,
|
deleteContainerSnapshot,
|
||||||
deletePortMapping,
|
deletePortMapping,
|
||||||
getContainer,
|
getContainer,
|
||||||
|
getContainerHistory,
|
||||||
getContainerSnapshots,
|
getContainerSnapshots,
|
||||||
getContainerUsage,
|
getContainerUsage,
|
||||||
getHostInfo,
|
getHostInfo,
|
||||||
@@ -209,39 +211,19 @@ export default function ContainerDetail() {
|
|||||||
}
|
}
|
||||||
}, [containerIdentifier, container?.snapshot_limit])
|
}, [containerIdentifier, container?.snapshot_limit])
|
||||||
|
|
||||||
const appendUsagePoint = useCallback((nextUsage: ContainerUsage, currentContainer: Container | null) => {
|
const fetchMetricHistory = useCallback(async () => {
|
||||||
if (!containerIdentifier || !currentContainer) return
|
if (!containerIdentifier) return
|
||||||
|
try {
|
||||||
const memoryTotalBytes = nextUsage.memory_total_bytes && nextUsage.memory_total_bytes > 0
|
const res = await getContainerHistory(containerIdentifier)
|
||||||
? nextUsage.memory_total_bytes
|
const points = (res.data.data || []).map(normalizeContainerMetricSample)
|
||||||
: currentContainer.ram_mb * 1024 * 1024
|
if (points.length > 0) {
|
||||||
const memoryPct = memoryTotalBytes > 0
|
setHistory(points)
|
||||||
? (nextUsage.memory_usage_bytes / memoryTotalBytes) * 100
|
localStorage.setItem(historyKey(container?.uuid || containerIdentifier), JSON.stringify(points))
|
||||||
: 0
|
|
||||||
const networkRx = nextUsage.network_rx_bps || 0
|
|
||||||
const networkTx = nextUsage.network_tx_bps || 0
|
|
||||||
const diskRead = nextUsage.disk_read_bps || 0
|
|
||||||
const diskWrite = nextUsage.disk_write_bps || 0
|
|
||||||
|
|
||||||
const point: MetricPoint = {
|
|
||||||
ts: Date.now(),
|
|
||||||
cpu: clamp((nextUsage.cpu_usage_pct || 0) / (currentContainer.vcpu || 1)),
|
|
||||||
memory: clamp(memoryPct),
|
|
||||||
network: networkRx + networkTx,
|
|
||||||
networkRx,
|
|
||||||
networkTx,
|
|
||||||
diskIO: diskRead + diskWrite,
|
|
||||||
diskRead,
|
|
||||||
diskWrite,
|
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
setHistory((prev) => {
|
console.error('Failed to fetch metric history:', err)
|
||||||
const cutoff = Date.now() - statsRanges['1w']
|
}
|
||||||
const next = [...prev.filter((item) => item.ts >= cutoff), point]
|
}, [containerIdentifier, container?.uuid])
|
||||||
localStorage.setItem(historyKey(currentContainer.uuid || containerIdentifier), JSON.stringify(next))
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, [containerIdentifier])
|
|
||||||
|
|
||||||
const fetchUsage = useCallback(async () => {
|
const fetchUsage = useCallback(async () => {
|
||||||
if (!containerIdentifier) return
|
if (!containerIdentifier) return
|
||||||
@@ -249,12 +231,11 @@ export default function ContainerDetail() {
|
|||||||
const res = await getContainerUsage(containerIdentifier)
|
const res = await getContainerUsage(containerIdentifier)
|
||||||
if (res.data.data) {
|
if (res.data.data) {
|
||||||
setUsage(res.data.data)
|
setUsage(res.data.data)
|
||||||
appendUsagePoint(res.data.data, container)
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch usage:', err)
|
console.error('Failed to fetch usage:', err)
|
||||||
}
|
}
|
||||||
}, [containerIdentifier, container, appendUsagePoint])
|
}, [containerIdentifier])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerIdentifier) return
|
if (!containerIdentifier) return
|
||||||
@@ -296,6 +277,12 @@ export default function ContainerDetail() {
|
|||||||
return () => window.clearInterval(timer)
|
return () => window.clearInterval(timer)
|
||||||
}, [fetchUsage])
|
}, [fetchUsage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMetricHistory()
|
||||||
|
const timer = window.setInterval(fetchMetricHistory, 30000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [fetchMetricHistory])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showSnapshots) fetchSnapshots()
|
if (showSnapshots) fetchSnapshots()
|
||||||
}, [showSnapshots, fetchSnapshots])
|
}, [showSnapshots, fetchSnapshots])
|
||||||
@@ -2492,6 +2479,20 @@ function readHistory(containerName: string): MetricPoint[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeContainerMetricSample(point: ContainerMetricSample): MetricPoint {
|
||||||
|
return {
|
||||||
|
ts: point.ts,
|
||||||
|
cpu: clamp(point.cpu),
|
||||||
|
memory: clamp(point.memory),
|
||||||
|
network: point.network || 0,
|
||||||
|
networkRx: point.network_rx || 0,
|
||||||
|
networkTx: point.network_tx || 0,
|
||||||
|
diskIO: point.disk_io || 0,
|
||||||
|
diskRead: point.disk_read || 0,
|
||||||
|
diskWrite: point.disk_write || 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function historyKey(containerName: string) {
|
function historyKey(containerName: string) {
|
||||||
return `clicd_container_metric_history:${containerName}`
|
return `clicd_container_metric_history:${containerName}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -967,6 +967,7 @@ function getTemplateName(id: string) {
|
|||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
'ubuntu-noble': 'Ubuntu 24.04',
|
'ubuntu-noble': 'Ubuntu 24.04',
|
||||||
'ubuntu-jammy': 'Ubuntu 22.04',
|
'ubuntu-jammy': 'Ubuntu 22.04',
|
||||||
|
'debian-trixie': 'Debian 13',
|
||||||
'debian-bookworm': 'Debian 12',
|
'debian-bookworm': 'Debian 12',
|
||||||
'debian-bullseye': 'Debian 11',
|
'debian-bullseye': 'Debian 11',
|
||||||
'alpine-3.21': 'Alpine 3.21',
|
'alpine-3.21': 'Alpine 3.21',
|
||||||
@@ -976,6 +977,8 @@ function getTemplateName(id: string) {
|
|||||||
'rockylinux-10': 'Rocky 10',
|
'rockylinux-10': 'Rocky 10',
|
||||||
'kvm-ubuntu-noble': 'Ubuntu 24.04',
|
'kvm-ubuntu-noble': 'Ubuntu 24.04',
|
||||||
'kvm-ubuntu-jammy': 'Ubuntu 22.04',
|
'kvm-ubuntu-jammy': 'Ubuntu 22.04',
|
||||||
|
'kvm-debian-trixie': 'Debian 13',
|
||||||
|
'kvm-debian-trixie-xfce': 'Debian 13 XFCE',
|
||||||
'kvm-debian-bookworm': 'Debian 12',
|
'kvm-debian-bookworm': 'Debian 12',
|
||||||
'kvm-debian-bullseye': 'Debian 11',
|
'kvm-debian-bullseye': 'Debian 11',
|
||||||
'kvm-rockylinux-9': 'Rocky 9',
|
'kvm-rockylinux-9': 'Rocky 9',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import ResourceStatsPanel, {
|
|||||||
StatsRangeKey,
|
StatsRangeKey,
|
||||||
statsRanges,
|
statsRanges,
|
||||||
} from '../components/ResourceStatsPanel'
|
} from '../components/ResourceStatsPanel'
|
||||||
import { DashboardStats, getDashboard, getHostInfo, HostInfo } from '../services/api'
|
import { DashboardStats, getDashboard, getHostHistory, getHostInfo, HostInfo, HostMetricPoint as HostMetricSample } from '../services/api'
|
||||||
|
|
||||||
type HostMetricPoint = {
|
type HostMetricPoint = {
|
||||||
ts: number
|
ts: number
|
||||||
@@ -30,6 +30,19 @@ export default function Dashboard() {
|
|||||||
const [range, setRange] = useState<StatsRangeKey>('30m')
|
const [range, setRange] = useState<StatsRangeKey>('30m')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const fetchHistory = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const res = await getHostHistory()
|
||||||
|
const points = (res.data.data || []).map(normalizeHostMetricSample)
|
||||||
|
if (points.length > 0) {
|
||||||
|
setHistory(points)
|
||||||
|
localStorage.setItem(hostHistoryKey, JSON.stringify(points))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const [dashRes, hostRes] = await Promise.all([getDashboard(), getHostInfo()])
|
const [dashRes, hostRes] = await Promise.all([getDashboard(), getHostInfo()])
|
||||||
@@ -37,7 +50,6 @@ export default function Dashboard() {
|
|||||||
if (hostRes.data.data) {
|
if (hostRes.data.data) {
|
||||||
const nextHost = hostRes.data.data
|
const nextHost = hostRes.data.data
|
||||||
setHost(nextHost)
|
setHost(nextHost)
|
||||||
appendHostPoint(nextHost, setHistory)
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -47,10 +59,15 @@ export default function Dashboard() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
fetchHistory()
|
||||||
fetchData()
|
fetchData()
|
||||||
const interval = window.setInterval(fetchData, 5000)
|
const interval = window.setInterval(fetchData, 5000)
|
||||||
return () => window.clearInterval(interval)
|
const historyInterval = window.setInterval(fetchHistory, 30000)
|
||||||
}, [fetchData])
|
return () => {
|
||||||
|
window.clearInterval(interval)
|
||||||
|
window.clearInterval(historyInterval)
|
||||||
|
}
|
||||||
|
}, [fetchData, fetchHistory])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -172,31 +189,6 @@ function SummaryCard({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendHostPoint(host: HostInfo, setHistory: (updater: (prev: HostMetricPoint[]) => HostMetricPoint[]) => void) {
|
|
||||||
const networkRx = host.network.rx_bps || 0
|
|
||||||
const networkTx = host.network.tx_bps || 0
|
|
||||||
const diskRead = host.disk_io.read_bps || 0
|
|
||||||
const diskWrite = host.disk_io.write_bps || 0
|
|
||||||
const point: HostMetricPoint = {
|
|
||||||
ts: Date.now(),
|
|
||||||
cpu: clamp(host.cpu.usage_pct),
|
|
||||||
memory: host.ram.total_mb > 0 ? clamp((host.ram.used_mb / host.ram.total_mb) * 100) : 0,
|
|
||||||
network: networkRx + networkTx,
|
|
||||||
networkRx,
|
|
||||||
networkTx,
|
|
||||||
diskIO: diskRead + diskWrite,
|
|
||||||
diskRead,
|
|
||||||
diskWrite,
|
|
||||||
}
|
|
||||||
|
|
||||||
setHistory((prev) => {
|
|
||||||
const cutoff = Date.now() - statsRanges['1w']
|
|
||||||
const next = [...prev.filter((item) => item.ts >= cutoff), point]
|
|
||||||
localStorage.setItem(hostHistoryKey, JSON.stringify(next))
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function readHostHistory(): HostMetricPoint[] {
|
function readHostHistory(): HostMetricPoint[] {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(hostHistoryKey)
|
const raw = localStorage.getItem(hostHistoryKey)
|
||||||
@@ -221,6 +213,20 @@ function toChartPoints(history: HostMetricPoint[], key: keyof Omit<HostMetricPoi
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHostMetricSample(point: HostMetricSample): HostMetricPoint {
|
||||||
|
return {
|
||||||
|
ts: point.ts,
|
||||||
|
cpu: clamp(point.cpu),
|
||||||
|
memory: clamp(point.memory),
|
||||||
|
network: point.network || 0,
|
||||||
|
networkRx: point.network_rx || 0,
|
||||||
|
networkTx: point.network_tx || 0,
|
||||||
|
diskIO: point.disk_io || 0,
|
||||||
|
diskRead: point.disk_read || 0,
|
||||||
|
diskWrite: point.disk_write || 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clamp(value: number) {
|
function clamp(value: number) {
|
||||||
if (!Number.isFinite(value)) return 0
|
if (!Number.isFinite(value)) return 0
|
||||||
return Math.max(0, Math.min(value, 100))
|
return Math.max(0, Math.min(value, 100))
|
||||||
|
|||||||
@@ -247,6 +247,19 @@ export interface HostInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HostMetricPoint {
|
||||||
|
ts: number
|
||||||
|
cpu: number
|
||||||
|
memory: number
|
||||||
|
network: number
|
||||||
|
network_rx: number
|
||||||
|
network_tx: number
|
||||||
|
disk_io: number
|
||||||
|
disk_read: number
|
||||||
|
disk_write: number
|
||||||
|
disk_usage_pct: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface HostProbeReport {
|
export interface HostProbeReport {
|
||||||
generated_at: string
|
generated_at: string
|
||||||
hostname: string
|
hostname: string
|
||||||
@@ -355,6 +368,18 @@ export interface ContainerUsage {
|
|||||||
guest_metrics?: boolean
|
guest_metrics?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ContainerMetricPoint {
|
||||||
|
ts: number
|
||||||
|
cpu: number
|
||||||
|
memory: number
|
||||||
|
network: number
|
||||||
|
network_rx: number
|
||||||
|
network_tx: number
|
||||||
|
disk_io: number
|
||||||
|
disk_read: number
|
||||||
|
disk_write: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface APIResponse<T = unknown> {
|
export interface APIResponse<T = unknown> {
|
||||||
success: boolean
|
success: boolean
|
||||||
message?: string
|
message?: string
|
||||||
@@ -477,6 +502,9 @@ export const resetSSHPassword = (id: ContainerIdentifier, password?: string) =>
|
|||||||
export const getContainerUsage = (id: ContainerIdentifier) =>
|
export const getContainerUsage = (id: ContainerIdentifier) =>
|
||||||
api.get<APIResponse<ContainerUsage>>(`/containers/${id}/usage`)
|
api.get<APIResponse<ContainerUsage>>(`/containers/${id}/usage`)
|
||||||
|
|
||||||
|
export const getContainerHistory = (id: ContainerIdentifier) =>
|
||||||
|
api.get<APIResponse<ContainerMetricPoint[]>>(`/containers/${id}/history`)
|
||||||
|
|
||||||
export interface TrafficInfo {
|
export interface TrafficInfo {
|
||||||
total_used_bytes: number
|
total_used_bytes: number
|
||||||
rx_used_bytes: number
|
rx_used_bytes: number
|
||||||
@@ -669,6 +697,9 @@ export const getDashboard = () =>
|
|||||||
export const getHostInfo = () =>
|
export const getHostInfo = () =>
|
||||||
api.get<APIResponse<HostInfo>>('/host-info')
|
api.get<APIResponse<HostInfo>>('/host-info')
|
||||||
|
|
||||||
|
export const getHostHistory = () =>
|
||||||
|
api.get<APIResponse<HostMetricPoint[]>>('/host-history')
|
||||||
|
|
||||||
export const getHostReport = () =>
|
export const getHostReport = () =>
|
||||||
api.get<APIResponse<HostProbeReport>>('/host-report')
|
api.get<APIResponse<HostProbeReport>>('/host-report')
|
||||||
|
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ remove_clicd_lxc_image_cache() {
|
|||||||
for image in \
|
for image in \
|
||||||
"ubuntu noble amd64" \
|
"ubuntu noble amd64" \
|
||||||
"ubuntu jammy amd64" \
|
"ubuntu jammy amd64" \
|
||||||
|
"debian trixie amd64" \
|
||||||
"debian bookworm amd64" \
|
"debian bookworm amd64" \
|
||||||
"debian bullseye amd64" \
|
"debian bullseye amd64" \
|
||||||
"alpine 3.21 amd64" \
|
"alpine 3.21 amd64" \
|
||||||
@@ -546,6 +547,7 @@ remove_clicd_lxc_image_cache() {
|
|||||||
"rockylinux 10 amd64" \
|
"rockylinux 10 amd64" \
|
||||||
"ubuntu noble arm64" \
|
"ubuntu noble arm64" \
|
||||||
"ubuntu jammy arm64" \
|
"ubuntu jammy arm64" \
|
||||||
|
"debian trixie arm64" \
|
||||||
"debian bookworm arm64" \
|
"debian bookworm arm64" \
|
||||||
"debian bullseye arm64" \
|
"debian bullseye arm64" \
|
||||||
"alpine 3.21 arm64" \
|
"alpine 3.21 arm64" \
|
||||||
|
|||||||
Reference in New Issue
Block a user