Support Debian 13.

#29
and fix useage chart data display problem, storage on database instead of localstorage.
This commit is contained in:
MengMengCode
2026-07-16 21:24:28 +08:00
parent 2ecdb5c26f
commit 5c6d6eafc9
13 changed files with 507 additions and 75 deletions
+35 -34
View File
@@ -32,6 +32,7 @@ import {
assignIPv6,
APIResponse,
Container,
ContainerMetricPoint as ContainerMetricSample,
ContainerUsage,
createSubUser,
createContainerSnapshot,
@@ -39,6 +40,7 @@ import {
deleteContainerSnapshot,
deletePortMapping,
getContainer,
getContainerHistory,
getContainerSnapshots,
getContainerUsage,
getHostInfo,
@@ -209,39 +211,19 @@ export default function ContainerDetail() {
}
}, [containerIdentifier, container?.snapshot_limit])
const appendUsagePoint = useCallback((nextUsage: ContainerUsage, currentContainer: Container | null) => {
if (!containerIdentifier || !currentContainer) return
const memoryTotalBytes = nextUsage.memory_total_bytes && nextUsage.memory_total_bytes > 0
? nextUsage.memory_total_bytes
: currentContainer.ram_mb * 1024 * 1024
const memoryPct = memoryTotalBytes > 0
? (nextUsage.memory_usage_bytes / memoryTotalBytes) * 100
: 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,
const fetchMetricHistory = useCallback(async () => {
if (!containerIdentifier) return
try {
const res = await getContainerHistory(containerIdentifier)
const points = (res.data.data || []).map(normalizeContainerMetricSample)
if (points.length > 0) {
setHistory(points)
localStorage.setItem(historyKey(container?.uuid || containerIdentifier), JSON.stringify(points))
}
} catch (err) {
console.error('Failed to fetch metric history:', err)
}
setHistory((prev) => {
const cutoff = Date.now() - statsRanges['1w']
const next = [...prev.filter((item) => item.ts >= cutoff), point]
localStorage.setItem(historyKey(currentContainer.uuid || containerIdentifier), JSON.stringify(next))
return next
})
}, [containerIdentifier])
}, [containerIdentifier, container?.uuid])
const fetchUsage = useCallback(async () => {
if (!containerIdentifier) return
@@ -249,12 +231,11 @@ export default function ContainerDetail() {
const res = await getContainerUsage(containerIdentifier)
if (res.data.data) {
setUsage(res.data.data)
appendUsagePoint(res.data.data, container)
}
} catch (err) {
console.error('Failed to fetch usage:', err)
}
}, [containerIdentifier, container, appendUsagePoint])
}, [containerIdentifier])
useEffect(() => {
if (!containerIdentifier) return
@@ -296,6 +277,12 @@ export default function ContainerDetail() {
return () => window.clearInterval(timer)
}, [fetchUsage])
useEffect(() => {
fetchMetricHistory()
const timer = window.setInterval(fetchMetricHistory, 30000)
return () => window.clearInterval(timer)
}, [fetchMetricHistory])
useEffect(() => {
if (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) {
return `clicd_container_metric_history:${containerName}`
}
+3
View File
@@ -967,6 +967,7 @@ function getTemplateName(id: string) {
const map: Record<string, string> = {
'ubuntu-noble': 'Ubuntu 24.04',
'ubuntu-jammy': 'Ubuntu 22.04',
'debian-trixie': 'Debian 13',
'debian-bookworm': 'Debian 12',
'debian-bullseye': 'Debian 11',
'alpine-3.21': 'Alpine 3.21',
@@ -976,6 +977,8 @@ function getTemplateName(id: string) {
'rockylinux-10': 'Rocky 10',
'kvm-ubuntu-noble': 'Ubuntu 24.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-bullseye': 'Debian 11',
'kvm-rockylinux-9': 'Rocky 9',
+35 -29
View File
@@ -7,7 +7,7 @@ import ResourceStatsPanel, {
StatsRangeKey,
statsRanges,
} 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 = {
ts: number
@@ -30,6 +30,19 @@ export default function Dashboard() {
const [range, setRange] = useState<StatsRangeKey>('30m')
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 () => {
try {
const [dashRes, hostRes] = await Promise.all([getDashboard(), getHostInfo()])
@@ -37,7 +50,6 @@ export default function Dashboard() {
if (hostRes.data.data) {
const nextHost = hostRes.data.data
setHost(nextHost)
appendHostPoint(nextHost, setHistory)
}
} catch (err) {
console.error(err)
@@ -47,10 +59,15 @@ export default function Dashboard() {
}, [])
useEffect(() => {
fetchHistory()
fetchData()
const interval = window.setInterval(fetchData, 5000)
return () => window.clearInterval(interval)
}, [fetchData])
const historyInterval = window.setInterval(fetchHistory, 30000)
return () => {
window.clearInterval(interval)
window.clearInterval(historyInterval)
}
}, [fetchData, fetchHistory])
if (loading) {
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[] {
try {
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) {
if (!Number.isFinite(value)) return 0
return Math.max(0, Math.min(value, 100))
+31
View File
@@ -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 {
generated_at: string
hostname: string
@@ -355,6 +368,18 @@ export interface ContainerUsage {
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> {
success: boolean
message?: string
@@ -477,6 +502,9 @@ export const resetSSHPassword = (id: ContainerIdentifier, password?: string) =>
export const getContainerUsage = (id: ContainerIdentifier) =>
api.get<APIResponse<ContainerUsage>>(`/containers/${id}/usage`)
export const getContainerHistory = (id: ContainerIdentifier) =>
api.get<APIResponse<ContainerMetricPoint[]>>(`/containers/${id}/history`)
export interface TrafficInfo {
total_used_bytes: number
rx_used_bytes: number
@@ -669,6 +697,9 @@ export const getDashboard = () =>
export const getHostInfo = () =>
api.get<APIResponse<HostInfo>>('/host-info')
export const getHostHistory = () =>
api.get<APIResponse<HostMetricPoint[]>>('/host-history')
export const getHostReport = () =>
api.get<APIResponse<HostProbeReport>>('/host-report')