mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-05 05:36:07 +08:00
release: v1.1.5
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
func getRootDiskInfo() (DiskInfo, bool) {
|
||||||
|
return DiskInfo{}, false
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package api
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
func getRootDiskInfo() (DiskInfo, bool) {
|
||||||
|
var stat unix.Statfs_t
|
||||||
|
if err := unix.Statfs("/", &stat); err != nil {
|
||||||
|
return DiskInfo{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
total := float64(int64(stat.Blocks)*int64(stat.Bsize)) / (1024 * 1024 * 1024)
|
||||||
|
free := float64(int64(stat.Bavail)*int64(stat.Bsize)) / (1024 * 1024 * 1024)
|
||||||
|
|
||||||
|
return DiskInfo{
|
||||||
|
TotalGB: total,
|
||||||
|
UsedGB: total - free,
|
||||||
|
FreeGB: free,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
+1198
-14
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,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-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", corsMiddleware(api.AdminMiddleware(api.HandleRouting)))
|
mux.HandleFunc("/api/routing", corsMiddleware(api.AdminMiddleware(api.HandleRouting)))
|
||||||
mux.HandleFunc("/api/ipv6/status", corsMiddleware(api.AdminMiddleware(api.HandleIPv6Status)))
|
mux.HandleFunc("/api/ipv6/status", corsMiddleware(api.AdminMiddleware(api.HandleIPv6Status)))
|
||||||
@@ -125,6 +126,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-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", corsMiddleware(api.AuthMiddleware(api.HandleRouting)))
|
mux.HandleFunc("/api/v1/routing", corsMiddleware(api.AuthMiddleware(api.HandleRouting)))
|
||||||
mux.HandleFunc("/api/v1/ipv6/status", corsMiddleware(api.AuthMiddleware(api.HandleIPv6Status)))
|
mux.HandleFunc("/api/v1/ipv6/status", corsMiddleware(api.AuthMiddleware(api.HandleIPv6Status)))
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
|
?
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Version = "1.1.4"
|
Version = "1.1.5"
|
||||||
Repo = "MengMengCode/CLICD"
|
Repo = "MengMengCode/CLICD"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "clicd-frontend",
|
"name": "clicd-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ContainerDetail from './pages/ContainerDetail'
|
|||||||
import Security from './pages/Security'
|
import Security from './pages/Security'
|
||||||
import AuditLogs from './pages/AuditLogs'
|
import AuditLogs from './pages/AuditLogs'
|
||||||
import ApiIntegration from './pages/ApiIntegration'
|
import ApiIntegration from './pages/ApiIntegration'
|
||||||
|
import HostReport from './pages/HostReport'
|
||||||
import Settings from './pages/Settings'
|
import Settings from './pages/Settings'
|
||||||
import ImageManagement from './pages/ImageManagement'
|
import ImageManagement from './pages/ImageManagement'
|
||||||
import Snapshots from './pages/Snapshots'
|
import Snapshots from './pages/Snapshots'
|
||||||
@@ -64,6 +65,7 @@ function App() {
|
|||||||
<Route path="routing" element={<Routing />} />
|
<Route path="routing" element={<Routing />} />
|
||||||
<Route path="audit-logs" element={<AuditLogs />} />
|
<Route path="audit-logs" element={<AuditLogs />} />
|
||||||
<Route path="api-integration" element={<ApiIntegration />} />
|
<Route path="api-integration" element={<ApiIntegration />} />
|
||||||
|
<Route path="host-report" element={<HostReport />} />
|
||||||
<Route path="sub-users" element={<SubUserManagement />} />
|
<Route path="sub-users" element={<SubUserManagement />} />
|
||||||
<Route path="settings" element={<Settings />} />
|
<Route path="settings" element={<Settings />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Code2,
|
Code2,
|
||||||
|
Cpu,
|
||||||
Camera,
|
Camera,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
@@ -71,6 +72,7 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
|||||||
const isRoutingPage = location.pathname.startsWith('/routing')
|
const isRoutingPage = location.pathname.startsWith('/routing')
|
||||||
const isAuditLogsPage = location.pathname.startsWith('/audit-logs')
|
const isAuditLogsPage = location.pathname.startsWith('/audit-logs')
|
||||||
const isApiIntegrationPage = location.pathname.startsWith('/api-integration')
|
const isApiIntegrationPage = location.pathname.startsWith('/api-integration')
|
||||||
|
const isHostReportPage = location.pathname.startsWith('/host-report')
|
||||||
const isSecurityPage = location.pathname.startsWith('/security')
|
const isSecurityPage = location.pathname.startsWith('/security')
|
||||||
const isSettingsPage = location.pathname.startsWith('/settings')
|
const isSettingsPage = location.pathname.startsWith('/settings')
|
||||||
|
|
||||||
@@ -222,6 +224,18 @@ export default function Sidebar({ collapsed, onToggle }: SidebarProps) {
|
|||||||
{!collapsed && <span>API 集成</span>}
|
{!collapsed && <span>API 集成</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/host-report')}
|
||||||
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||||
|
isHostReportPage
|
||||||
|
? 'bg-black text-white dark:bg-white dark:text-black'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Cpu className="w-4 h-4" />
|
||||||
|
{!collapsed && <span>宿主机信息</span>}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/settings')}
|
onClick={() => navigate('/settings')}
|
||||||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-md text-sm transition-colors ${
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import { ReactNode, useCallback, useEffect, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Activity,
|
||||||
|
CheckCircle2,
|
||||||
|
Cpu,
|
||||||
|
HardDrive,
|
||||||
|
MemoryStick,
|
||||||
|
RefreshCw,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { getHostReport, HostProbeReport } from '../services/api'
|
||||||
|
|
||||||
|
export default function HostReport() {
|
||||||
|
const [report, setReport] = useState<HostProbeReport | null>(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
const fetchReport = useCallback(async () => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const res = await getHostReport()
|
||||||
|
setReport(res.data.data || null)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchReport()
|
||||||
|
}, [fetchReport])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-black">宿主机信息</h1>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">硬件、网络、磁盘健康与运行环境探测报告</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={fetchReport} disabled={loading} className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 px-3 py-2 text-sm text-gray-600 hover:bg-gray-50 disabled:opacity-50">
|
||||||
|
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
刷新
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && !report ? (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white py-14 text-center text-sm text-gray-400">正在探测宿主机环境...</div>
|
||||||
|
) : !report ? (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white py-14 text-center text-sm text-gray-400">暂未获取到宿主机信息</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<ProbeMetric icon={<Cpu className="h-4 w-4" />} label="CPU" value={report.cpu.model || 'Unknown'} sub={`${report.cpu.cores} 核 / ${report.cpu.threads} 线程`} />
|
||||||
|
<ProbeMetric icon={<MemoryStick className="h-4 w-4" />} label="RAM" value={formatMB(report.memory.total_mb)} sub={`${formatMB(report.memory.used_mb)} 已用`} />
|
||||||
|
<ProbeMetric icon={<HardDrive className="h-4 w-4" />} label="DISK" value={`${report.disks.length} 块硬盘`} sub={report.disks.map(d => d.type).filter(Boolean).join(' / ') || 'Unknown'} />
|
||||||
|
<ProbeMetric icon={<Activity className="h-4 w-4" />} label="运行状态" value={report.system.uptime_text} sub={`${report.system.process_count} 个进程`} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProbeSection title="系统概览">
|
||||||
|
<ProbeRows rows={[
|
||||||
|
['主机名', report.hostname],
|
||||||
|
['操作系统', report.os],
|
||||||
|
['内核', report.kernel],
|
||||||
|
['生成时间', report.generated_at],
|
||||||
|
['CPU 架构', report.cpu.architecture],
|
||||||
|
['CPU 虚拟化指令', report.cpu.virtualization ? `支持 (${report.cpu.virtualization_key})` : '未检测到'],
|
||||||
|
['CPU 核显', report.cpu.has_integrated_gpu ? '检测到' : '未检测到'],
|
||||||
|
['显卡', report.gpus.length ? `${report.gpus.length} 个` : '未检测到'],
|
||||||
|
['运行能力', runtimeModeLabel(report.runtime.support_mode)],
|
||||||
|
['KVM 嵌套虚拟化', `${report.runtime.nested_virtualization ? '支持' : '未检测到'} (${report.runtime.nested_detail || '-'})`],
|
||||||
|
]} />
|
||||||
|
</ProbeSection>
|
||||||
|
|
||||||
|
<ProbeSection title="公网与路由">
|
||||||
|
<ProbeRows rows={[
|
||||||
|
['公网 IPv4', report.public_ipv4.length ? report.public_ipv4.join('\n') : '未检测到'],
|
||||||
|
['IPv4 地址', report.ipv4_addresses?.length ? report.ipv4_addresses.map(formatIPv4Address).join('\n') : '未检测到'],
|
||||||
|
['IPv4 段', report.ipv4_prefixes?.length ? report.ipv4_prefixes.map(formatIPv4Prefix).join('\n') : '未检测到'],
|
||||||
|
['IPv6 地址', report.ipv6_addresses.length ? report.ipv6_addresses.map(ip => `${ip.address}/${ip.prefix_len} (${ip.interface})`).join('\n') : '未检测到'],
|
||||||
|
['IPv6 段', report.ipv6_prefixes?.length ? report.ipv6_prefixes.map(formatIPv6Prefix).join('\n') : '未检测到'],
|
||||||
|
['网关', report.gateways.length ? report.gateways.map(g => `${g.family}: ${g.gateway || '-'} dev ${g.interface || '-'}`).join('\n') : '未检测到'],
|
||||||
|
]} />
|
||||||
|
</ProbeSection>
|
||||||
|
|
||||||
|
<ProbeTable
|
||||||
|
title="内存条"
|
||||||
|
empty="未检测到内存条明细,可能缺少 dmidecode 或权限受限"
|
||||||
|
headers={['插槽', '容量', '类型', '频率', '厂商', '型号/序列号']}
|
||||||
|
rows={(report.memory.modules || []).map(m => [
|
||||||
|
m.locator || '-',
|
||||||
|
m.size || '-',
|
||||||
|
m.type || '-',
|
||||||
|
m.speed || '-',
|
||||||
|
m.manufacturer || '-',
|
||||||
|
[m.part_number, m.serial_number].filter(Boolean).join(' / ') || '-',
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProbeTable
|
||||||
|
title="硬盘与健康"
|
||||||
|
empty="未检测到硬盘"
|
||||||
|
headers={['设备', '型号', '容量', '类型', '挂载点', '健康', '寿命', '通电', '读取', '写入', '命令数', '擦写']}
|
||||||
|
rows={report.disks.map(d => [
|
||||||
|
`${d.path || d.name}\n${d.serial || ''}`,
|
||||||
|
d.model || '-',
|
||||||
|
formatBytes(d.size_bytes),
|
||||||
|
d.type || (d.rotational ? 'HDD' : 'SSD'),
|
||||||
|
d.mountpoints?.length ? d.mountpoints.join('\n') : '-',
|
||||||
|
`${diskHealthLabel(d.health)}\n${d.health_detail || ''}`,
|
||||||
|
formatLifeUsed(d.smart?.life_used_percent),
|
||||||
|
d.smart?.power_on_hours ? `${d.smart.power_on_hours} 小时\n${formatPowerOnDays(d.smart.power_on_hours)}` : '-',
|
||||||
|
formatBytes(d.smart?.read_data_bytes || 0),
|
||||||
|
formatBytes(d.smart?.written_data_bytes || 0),
|
||||||
|
formatCommands(d.smart?.read_commands, d.smart?.write_commands),
|
||||||
|
formatWear(d.smart?.wear_leveling_count, d.smart?.erase_count, d.smart?.power_cycle_count),
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProbeTable
|
||||||
|
title="网卡"
|
||||||
|
empty="未检测到网卡"
|
||||||
|
headers={['网卡', '状态', '驱动/速率', 'MAC', 'IPv4', 'IPv6']}
|
||||||
|
rows={report.network_interfaces.map(n => [
|
||||||
|
`${n.name}\n${n.model || ''}`,
|
||||||
|
n.state || '-',
|
||||||
|
`${n.driver || '-'}\n${n.speed_mbps > 0 ? `${n.speed_mbps} Mbps` : '-'}`,
|
||||||
|
n.mac || '-',
|
||||||
|
n.ipv4?.length ? n.ipv4.map(ip => `${ip.address}/${ip.prefix_len}`).join('\n') : '-',
|
||||||
|
n.ipv6?.length ? n.ipv6.map(ip => `${ip.address}/${ip.prefix_len} ${ip.scope}`).join('\n') : '-',
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProbeTable
|
||||||
|
title="显卡"
|
||||||
|
empty="未检测到显卡"
|
||||||
|
headers={['名称', '厂商', '类型', '驱动']}
|
||||||
|
rows={report.gpus.map(g => [g.name, g.vendor || '-', gpuTypeLabel(g.type), g.driver || '-'])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProbeSection title="环境支持">
|
||||||
|
<div className="grid gap-2 md:grid-cols-2">
|
||||||
|
{report.environment.map(item => (
|
||||||
|
<div key={item.key} className="flex items-start gap-2 rounded-lg border border-gray-200 bg-white px-3 py-2">
|
||||||
|
{item.ok ? <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-green-600" /> : <XCircle className={`mt-0.5 h-4 w-4 shrink-0 ${item.required ? 'text-red-600' : 'text-amber-600'}`} />}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 text-xs font-medium text-gray-800">
|
||||||
|
<span>{item.label}</span>
|
||||||
|
<span className={`rounded px-1.5 py-0.5 text-[10px] ${item.required ? 'bg-gray-100 text-gray-600' : 'bg-blue-50 text-blue-700'}`}>
|
||||||
|
{item.required ? '必要' : '可选'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 break-all font-mono text-[11px] text-gray-500">{item.detail || '-'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ProbeSection>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProbeMetric({ icon, label, value, sub }: { icon: ReactNode; label: string; value: string; sub: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white px-3 py-3">
|
||||||
|
<div className="mb-2 flex items-center gap-2 text-xs font-medium text-gray-500">
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div className="line-clamp-2 break-words text-sm font-semibold text-gray-900" title={value}>{value}</div>
|
||||||
|
<div className="mt-1 truncate text-xs text-gray-500" title={sub}>{sub}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProbeSection({ title, children }: { title: string; children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-2 text-sm font-semibold text-black">{title}</h2>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProbeRows({ rows }: { rows: Array<[string, string]> }) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-lg border border-gray-200 bg-white">
|
||||||
|
{rows.map(([label, value]) => (
|
||||||
|
<div key={label} className="grid gap-2 border-b border-gray-100 px-3 py-2 text-xs last:border-b-0 md:grid-cols-[160px_1fr]">
|
||||||
|
<div className="font-medium text-gray-500">{label}</div>
|
||||||
|
<div className="whitespace-pre-wrap break-words font-mono text-gray-800">{value || '-'}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProbeTable({ title, headers, rows, empty }: { title: string; headers: string[]; rows: string[][]; empty: string }) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-2 text-sm font-semibold text-black">{title}</h2>
|
||||||
|
{rows.length === 0 ? (
|
||||||
|
<div className="rounded-lg border border-gray-200 bg-white px-3 py-3 text-xs text-gray-400">{empty}</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto rounded-lg border border-gray-200 bg-white">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-100 bg-gray-50 text-left text-gray-500">
|
||||||
|
{headers.map(header => <th key={header} className="px-3 py-2 font-medium">{header}</th>)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-100">
|
||||||
|
{rows.map((row, rowIndex) => (
|
||||||
|
<tr key={rowIndex} className="align-top">
|
||||||
|
{row.map((cell, cellIndex) => (
|
||||||
|
<td key={cellIndex} className="max-w-[280px] whitespace-pre-wrap break-words px-3 py-2 text-gray-700">
|
||||||
|
{cell || '-'}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIPv4Address(ip: HostProbeReport['ipv4_addresses'][number]) {
|
||||||
|
return `${ip.address}/${ip.prefix_len} (${ip.interface})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIPv4Prefix(prefix: HostProbeReport['ipv4_prefixes'][number]) {
|
||||||
|
const parts = [
|
||||||
|
prefix.prefix || '-',
|
||||||
|
prefix.subnet_mask ? `mask ${prefix.subnet_mask}` : '',
|
||||||
|
prefix.gateway ? `via ${prefix.gateway}` : '',
|
||||||
|
prefix.interface ? `dev ${prefix.interface}` : '',
|
||||||
|
prefix.source ? `[${prefix.source}]` : '',
|
||||||
|
].filter(Boolean)
|
||||||
|
return parts.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatIPv6Prefix(prefix: HostProbeReport['ipv6_prefixes'][number]) {
|
||||||
|
const value = prefix.prefix || prefix.address || '-'
|
||||||
|
const cidr = value.includes('/') || !prefix.prefix_len ? value : `${value}/${prefix.prefix_len}`
|
||||||
|
return `${cidr} via ${prefix.gateway || '-'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMB(value: number) {
|
||||||
|
if (!value) return '-'
|
||||||
|
if (value >= 1024) return `${(value / 1024).toFixed(1)} GB`
|
||||||
|
return `${value} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(value: number) {
|
||||||
|
if (!value) return '-'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||||
|
let next = value
|
||||||
|
let index = 0
|
||||||
|
while (next >= 1024 && index < units.length - 1) {
|
||||||
|
next /= 1024
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return `${next.toFixed(index === 0 ? 0 : 1)} ${units[index]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLifeUsed(value?: number) {
|
||||||
|
if (value === undefined || value === null) return '-'
|
||||||
|
return `${value}% 已用\n${Math.max(0, 100 - value)}% 剩余`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPowerOnDays(hours: number) {
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
const rest = hours % 24
|
||||||
|
return days > 0 ? `${days} 天 ${rest} 小时` : `${hours} 小时`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCommands(read?: number, write?: number) {
|
||||||
|
if (!read && !write) return '-'
|
||||||
|
return `读 ${formatCount(read || 0)}\n写 ${formatCount(write || 0)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCount(value: number) {
|
||||||
|
if (!value) return '-'
|
||||||
|
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`
|
||||||
|
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
|
||||||
|
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`
|
||||||
|
return `${value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWear(wear?: string, erase?: string, powerCycles?: number) {
|
||||||
|
const rows: string[] = []
|
||||||
|
if (wear) rows.push(`磨损 ${wear}`)
|
||||||
|
if (erase) rows.push(`擦写 ${erase}`)
|
||||||
|
if (powerCycles) rows.push(`启停 ${powerCycles}`)
|
||||||
|
return rows.length ? rows.join('\n') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeModeLabel(value: string) {
|
||||||
|
switch (value) {
|
||||||
|
case 'kvm_lxc':
|
||||||
|
return '支持 KVM + LXC'
|
||||||
|
case 'lxc_only':
|
||||||
|
return '仅支持 LXC'
|
||||||
|
default:
|
||||||
|
return '未满足运行环境'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function diskHealthLabel(value: string) {
|
||||||
|
switch (value) {
|
||||||
|
case 'ok':
|
||||||
|
return '健康'
|
||||||
|
case 'failed':
|
||||||
|
return '异常'
|
||||||
|
default:
|
||||||
|
return '未知'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gpuTypeLabel(value: string) {
|
||||||
|
if (value === 'integrated') return '核显'
|
||||||
|
if (value === 'discrete') return '独显'
|
||||||
|
return value || '-'
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ export default function Login() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.4</p>
|
<p className="text-center text-xs text-gray-400 mt-6">CLICD v1.1.5</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { UserCog, Key, LogIn, Monitor, Clock, Globe } from 'lucide-react'
|
import { Clock, Globe, LogIn, Monitor, UserCog } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
changePassword,
|
changePassword,
|
||||||
changeUsername,
|
changeUsername,
|
||||||
@@ -20,7 +20,6 @@ export default function Settings() {
|
|||||||
const [oldPwd, setOldPwd] = useState('')
|
const [oldPwd, setOldPwd] = useState('')
|
||||||
const [newPwd, setNewPwd] = useState('')
|
const [newPwd, setNewPwd] = useState('')
|
||||||
const [newUsername, setNewUsername] = useState('')
|
const [newUsername, setNewUsername] = useState('')
|
||||||
const [pwdForUser, setPwdForUser] = useState('')
|
|
||||||
|
|
||||||
const fetchLogs = useCallback(async () => {
|
const fetchLogs = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -33,30 +32,45 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => { fetchLogs(); const t = setInterval(fetchLogs, 15000); return () => clearInterval(t) }, [fetchLogs])
|
useEffect(() => {
|
||||||
|
fetchLogs()
|
||||||
|
const timer = setInterval(fetchLogs, 15000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [fetchLogs])
|
||||||
|
|
||||||
const handleSaveAccount = async () => {
|
const handleSaveAccount = async () => {
|
||||||
if (!oldPwd) { dialog.alert('提示', '请输入当前密码以确认修改'); return }
|
if (!oldPwd) {
|
||||||
if (!newPwd && !newUsername) { dialog.alert('提示', '至少填写新密码或新用户名中的一项'); return }
|
dialog.alert('提示', '请输入当前密码以确认修改')
|
||||||
if (newPwd && newPwd.length < 6) { dialog.alert('提示', '新密码至少 6 位'); return }
|
return
|
||||||
if (newUsername && newUsername.length < 3) { dialog.alert('提示', '用户名至少 3 位'); return }
|
}
|
||||||
|
if (!newPwd && !newUsername) {
|
||||||
|
dialog.alert('提示', '至少填写新密码或新用户名中的一项')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (newPwd && newPwd.length < 6) {
|
||||||
|
dialog.alert('提示', '新密码至少 6 位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (newUsername && newUsername.length < 3) {
|
||||||
|
dialog.alert('提示', '用户名至少 3 位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let results: string[] = []
|
const results: string[] = []
|
||||||
try {
|
try {
|
||||||
// 先改用户名(用旧密码验证),再改密码,否则改完密码后旧密码就失效了
|
|
||||||
if (newUsername) {
|
if (newUsername) {
|
||||||
const res = await changeUsername(newUsername, oldPwd)
|
const res = await changeUsername(newUsername, oldPwd)
|
||||||
if (res.data.success) results.push('用户名已修改')
|
results.push(res.data.success ? '用户名已修改' : '用户名修改失败')
|
||||||
else results.push('用户名修改失败')
|
|
||||||
}
|
}
|
||||||
if (newPwd) {
|
if (newPwd) {
|
||||||
const res = await changePassword(oldPwd, newPwd)
|
const res = await changePassword(oldPwd, newPwd)
|
||||||
if (res.data.success) results.push('密码已修改')
|
results.push(res.data.success ? '密码已修改' : '密码修改失败')
|
||||||
else results.push('密码修改失败')
|
|
||||||
}
|
}
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
dialog.alert('完成', results.join(',') + '。下次登录生效')
|
dialog.alert('完成', `${results.join(',')}。下次登录生效`)
|
||||||
setOldPwd(''); setNewPwd(''); setNewUsername('')
|
setOldPwd('')
|
||||||
|
setNewPwd('')
|
||||||
|
setNewUsername('')
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const e = err as { response?: { data?: { message?: string } } }
|
const e = err as { response?: { data?: { message?: string } } }
|
||||||
@@ -67,48 +81,48 @@ export default function Settings() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center py-20">
|
<div className="flex items-center justify-center py-20">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-black"></div>
|
<div className="h-8 w-8 animate-spin rounded-full border-b-2 border-black"></div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(logs.length / pageSize)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-black">面板设置</h1>
|
<h1 className="text-2xl font-bold text-black">面板设置</h1>
|
||||||
<p className="text-sm text-gray-500 mt-1">账号管理与登录日志</p>
|
<p className="mt-1 text-sm text-gray-500">账号管理与登录日志</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Account Settings */}
|
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-black">
|
||||||
<h2 className="text-sm font-semibold text-black mb-4 flex items-center gap-2">
|
<UserCog className="h-4 w-4" />账号设置
|
||||||
<UserCog className="w-4 h-4" />账号设置
|
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-500 mb-1">当前用户名</label>
|
<label className="mb-1 block text-xs text-gray-500">当前用户名</label>
|
||||||
<input type="text" value={username || ''} disabled className="w-full px-3 py-2 border border-gray-200 rounded-md text-sm text-gray-400 bg-gray-50" />
|
<input type="text" value={username || ''} disabled className="w-full rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-500 mb-1">新用户名(留空则不修改)</label>
|
<label className="mb-1 block text-xs text-gray-500">新用户名(留空则不修改)</label>
|
||||||
<input type="text" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-black bg-white" placeholder="至少 3 位" />
|
<input type="text" value={newUsername} onChange={(e) => setNewUsername(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="至少 3 位" />
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-100 pt-3">
|
<div className="border-t border-gray-100 pt-3">
|
||||||
<label className="block text-xs text-gray-500 mb-1">新密码(留空则不修改)</label>
|
<label className="mb-1 block text-xs text-gray-500">新密码(留空则不修改)</label>
|
||||||
<input type="password" value={newPwd} onChange={(e) => setNewPwd(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-black bg-white" placeholder="至少 6 位" />
|
<input type="password" value={newPwd} onChange={(e) => setNewPwd(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="至少 6 位" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs text-gray-500 mb-1">当前密码(验证身份)</label>
|
<label className="mb-1 block text-xs text-gray-500">当前密码(验证身份)</label>
|
||||||
<input type="password" value={oldPwd} onChange={(e) => setOldPwd(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm text-black bg-white" placeholder="输入当前密码以确认修改" />
|
<input type="password" value={oldPwd} onChange={(e) => setOldPwd(e.target.value)} className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-black" placeholder="输入当前密码以确认修改" />
|
||||||
</div>
|
</div>
|
||||||
<button onClick={handleSaveAccount} className="w-full px-4 py-2 bg-black text-white rounded-md text-sm hover:bg-gray-800">保存修改</button>
|
<button onClick={handleSaveAccount} className="w-full rounded-md bg-black px-4 py-2 text-sm text-white hover:bg-gray-800">保存修改</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login Logs */}
|
<div className="rounded-lg border border-gray-200 bg-white p-5">
|
||||||
<div className="bg-white border border-gray-200 rounded-lg p-5">
|
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-black">
|
||||||
<h2 className="text-sm font-semibold text-black mb-4 flex items-center gap-2">
|
<LogIn className="h-4 w-4" />登录日志
|
||||||
<LogIn className="w-4 h-4" />登录日志
|
|
||||||
</h2>
|
</h2>
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<p className="text-sm text-gray-400">暂无登录记录</p>
|
<p className="text-sm text-gray-400">暂无登录记录</p>
|
||||||
@@ -117,23 +131,23 @@ export default function Settings() {
|
|||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-gray-400 border-b border-gray-100">
|
<tr className="border-b border-gray-100 text-gray-400">
|
||||||
<th className="text-left py-2 font-medium w-40"><span className="inline-flex items-center gap-1"><Clock className="w-3 h-3" />时间</span></th>
|
<th className="w-40 py-2 text-left font-medium"><span className="inline-flex items-center gap-1"><Clock className="h-3 w-3" />时间</span></th>
|
||||||
<th className="text-left py-2 font-medium">用户名</th>
|
<th className="py-2 text-left font-medium">用户名</th>
|
||||||
<th className="text-left py-2 font-medium"><span className="inline-flex items-center gap-1"><Globe className="w-3 h-3" />IP</span></th>
|
<th className="py-2 text-left font-medium"><span className="inline-flex items-center gap-1"><Globe className="h-3 w-3" />IP</span></th>
|
||||||
<th className="text-left py-2 font-medium"><span className="inline-flex items-center gap-1"><Monitor className="w-3 h-3" />设备</span></th>
|
<th className="py-2 text-left font-medium"><span className="inline-flex items-center gap-1"><Monitor className="h-3 w-3" />设备</span></th>
|
||||||
<th className="text-left py-2 font-medium">结果</th>
|
<th className="py-2 text-left font-medium">结果</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-50">
|
||||||
{logs.slice((logPage - 1) * pageSize, logPage * pageSize).map((log, i) => (
|
{logs.slice((logPage - 1) * pageSize, logPage * pageSize).map((log, index) => (
|
||||||
<tr key={i}>
|
<tr key={`${log.time}-${index}`}>
|
||||||
<td className="py-1.5 text-gray-500 font-mono whitespace-nowrap">{log.time}</td>
|
<td className="whitespace-nowrap py-1.5 font-mono text-gray-500">{log.time}</td>
|
||||||
<td className="py-1.5 text-gray-700">{log.username}</td>
|
<td className="py-1.5 text-gray-700">{log.username}</td>
|
||||||
<td className="py-1.5 text-gray-500 font-mono">{log.ip}</td>
|
<td className="py-1.5 font-mono text-gray-500">{log.ip}</td>
|
||||||
<td className="py-1.5 text-gray-500 max-w-[180px] truncate" title={log.user_agent}>{formatUA(log.user_agent)}</td>
|
<td className="max-w-[180px] truncate py-1.5 text-gray-500" title={log.user_agent}>{formatUA(log.user_agent)}</td>
|
||||||
<td className="py-1.5">
|
<td className="py-1.5">
|
||||||
<span className={`px-1.5 py-0.5 rounded text-xs ${log.success ? 'bg-gray-100 text-gray-700' : 'bg-red-50 text-red-600'}`}>
|
<span className={`rounded px-1.5 py-0.5 text-xs ${log.success ? 'bg-gray-100 text-gray-700' : 'bg-red-50 text-red-600'}`}>
|
||||||
{log.success ? '成功' : '失败'}
|
{log.success ? '成功' : '失败'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
@@ -143,23 +157,22 @@ export default function Settings() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{logs.length > pageSize && (
|
{logs.length > pageSize && (
|
||||||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100">
|
<div className="mt-3 flex items-center justify-between border-t border-gray-100 pt-3">
|
||||||
<span className="text-xs text-gray-400">共 {logs.length} 条,第 {logPage}/{Math.ceil(logs.length / pageSize)} 页</span>
|
<span className="text-xs text-gray-400">共 {logs.length} 条,第 {logPage}/{totalPages} 页</span>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button onClick={() => setLogPage(1)} disabled={logPage === 1} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">首页</button>
|
<button onClick={() => setLogPage(1)} disabled={logPage === 1} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">首页</button>
|
||||||
<button onClick={() => setLogPage(p => Math.max(1, p - 1))} disabled={logPage === 1} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">上一页</button>
|
<button onClick={() => setLogPage(p => Math.max(1, p - 1))} disabled={logPage === 1} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">上一页</button>
|
||||||
{Array.from({length: Math.min(5, Math.ceil(logs.length / pageSize))}, (_, i) => {
|
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||||
const totalPages = Math.ceil(logs.length / pageSize)
|
|
||||||
let start = Math.max(1, logPage - 2)
|
let start = Math.max(1, logPage - 2)
|
||||||
if (start + 4 > totalPages) start = Math.max(1, totalPages - 4)
|
if (start + 4 > totalPages) start = Math.max(1, totalPages - 4)
|
||||||
const page = start + i
|
const page = start + i
|
||||||
if (page > totalPages) return null
|
if (page > totalPages) return null
|
||||||
return (
|
return (
|
||||||
<button key={page} onClick={() => setLogPage(page)} className={`w-7 h-7 text-xs rounded ${page === logPage ? 'bg-black text-white' : 'border border-gray-200 hover:bg-gray-50'}`}>{page}</button>
|
<button key={page} onClick={() => setLogPage(page)} className={`h-7 w-7 rounded text-xs ${page === logPage ? 'bg-black text-white' : 'border border-gray-200 hover:bg-gray-50'}`}>{page}</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<button onClick={() => setLogPage(p => Math.min(Math.ceil(logs.length / pageSize), p + 1))} disabled={logPage >= Math.ceil(logs.length / pageSize)} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">下一页</button>
|
<button onClick={() => setLogPage(p => Math.min(totalPages, p + 1))} disabled={logPage >= totalPages} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">下一页</button>
|
||||||
<button onClick={() => setLogPage(Math.ceil(logs.length / pageSize))} disabled={logPage >= Math.ceil(logs.length / pageSize)} className="px-2 py-1 text-xs border border-gray-200 rounded hover:bg-gray-50 disabled:opacity-30">末页</button>
|
<button onClick={() => setLogPage(totalPages)} disabled={logPage >= totalPages} className="rounded border border-gray-200 px-2 py-1 text-xs hover:bg-gray-50 disabled:opacity-30">末页</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -171,7 +184,6 @@ export default function Settings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatUA(ua: string): string {
|
function formatUA(ua: string): string {
|
||||||
// Extract browser/OS info from UA string
|
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (ua.includes('Windows NT')) parts.push('Windows')
|
if (ua.includes('Windows NT')) parts.push('Windows')
|
||||||
else if (ua.includes('Mac OS X')) parts.push('macOS')
|
else if (ua.includes('Mac OS X')) parts.push('macOS')
|
||||||
|
|||||||
@@ -136,6 +136,16 @@ export interface IPv6Status {
|
|||||||
prefixes: IPv6PrefixInfo[]
|
prefixes: IPv6PrefixInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPv4PrefixInfo {
|
||||||
|
interface: string
|
||||||
|
address: string
|
||||||
|
prefix: string
|
||||||
|
prefix_len: number
|
||||||
|
subnet_mask: string
|
||||||
|
gateway: string
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface DashboardStats {
|
export interface DashboardStats {
|
||||||
total_containers: number
|
total_containers: number
|
||||||
running: number
|
running: number
|
||||||
@@ -161,6 +171,93 @@ export interface HostInfo {
|
|||||||
load: { load1: number; load5: number; load15: number }
|
load: { load1: number; load5: number; load15: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HostProbeReport {
|
||||||
|
generated_at: string
|
||||||
|
hostname: string
|
||||||
|
kernel: string
|
||||||
|
os: string
|
||||||
|
cpu: {
|
||||||
|
model: string
|
||||||
|
cores: number
|
||||||
|
threads: number
|
||||||
|
architecture: string
|
||||||
|
flags: string[]
|
||||||
|
has_integrated_gpu: boolean
|
||||||
|
virtualization: boolean
|
||||||
|
virtualization_key: string
|
||||||
|
}
|
||||||
|
memory: {
|
||||||
|
total_mb: number
|
||||||
|
used_mb: number
|
||||||
|
free_mb: number
|
||||||
|
modules: Array<{
|
||||||
|
locator: string
|
||||||
|
size: string
|
||||||
|
type: string
|
||||||
|
speed: string
|
||||||
|
manufacturer: string
|
||||||
|
part_number: string
|
||||||
|
serial_number: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
disks: Array<{
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
model: string
|
||||||
|
serial: string
|
||||||
|
size_bytes: number
|
||||||
|
type: string
|
||||||
|
rotational: boolean
|
||||||
|
mountpoints: string[]
|
||||||
|
health: string
|
||||||
|
health_detail: string
|
||||||
|
smart?: {
|
||||||
|
available: boolean
|
||||||
|
life_used_percent?: number
|
||||||
|
power_on_hours?: number
|
||||||
|
power_cycle_count?: number
|
||||||
|
read_data_bytes?: number
|
||||||
|
written_data_bytes?: number
|
||||||
|
read_commands?: number
|
||||||
|
write_commands?: number
|
||||||
|
wear_leveling_count?: string
|
||||||
|
erase_count?: string
|
||||||
|
media_errors?: number
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
network_interfaces: Array<{
|
||||||
|
name: string
|
||||||
|
mac: string
|
||||||
|
state: string
|
||||||
|
speed_mbps: number
|
||||||
|
driver: string
|
||||||
|
model: string
|
||||||
|
ipv4: Array<{ interface: string; address: string; prefix_len: number; scope: string; gateway?: string }>
|
||||||
|
ipv6: Array<{ interface: string; address: string; prefix_len: number; scope: string; gateway?: string }>
|
||||||
|
}>
|
||||||
|
public_ipv4: string[]
|
||||||
|
ipv4_addresses: Array<{ interface: string; address: string; prefix_len: number; scope: string; gateway?: string }>
|
||||||
|
ipv4_prefixes: IPv4PrefixInfo[]
|
||||||
|
ipv6_addresses: Array<{ interface: string; address: string; prefix_len: number; scope: string; gateway?: string }>
|
||||||
|
ipv6_prefixes: IPv6PrefixInfo[]
|
||||||
|
gateways: Array<{ family: string; interface: string; gateway: string }>
|
||||||
|
gpus: Array<{ name: string; vendor: string; driver: string; type: string }>
|
||||||
|
runtime: {
|
||||||
|
lxc_available: boolean
|
||||||
|
kvm_available: boolean
|
||||||
|
dev_kvm: boolean
|
||||||
|
nested_virtualization: boolean
|
||||||
|
nested_detail: string
|
||||||
|
support_mode: string
|
||||||
|
}
|
||||||
|
system: {
|
||||||
|
uptime_seconds: number
|
||||||
|
uptime_text: string
|
||||||
|
process_count: number
|
||||||
|
}
|
||||||
|
environment: Array<{ key: string; label: string; ok: boolean; required: boolean; detail: string }>
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContainerUsage {
|
export interface ContainerUsage {
|
||||||
memory_usage_bytes: number
|
memory_usage_bytes: number
|
||||||
memory_total_bytes?: number
|
memory_total_bytes?: number
|
||||||
@@ -393,6 +490,9 @@ export const getDashboard = () =>
|
|||||||
export const getHostInfo = () =>
|
export const getHostInfo = () =>
|
||||||
api.get<APIResponse<HostInfo>>('/host-info')
|
api.get<APIResponse<HostInfo>>('/host-info')
|
||||||
|
|
||||||
|
export const getHostReport = () =>
|
||||||
|
api.get<APIResponse<HostProbeReport>>('/host-report')
|
||||||
|
|
||||||
// Snapshots
|
// Snapshots
|
||||||
export interface Snapshot {
|
export interface Snapshot {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
Reference in New Issue
Block a user