feat(monitoring): add Beszel fleet integration to Monitor page
- New api/services/beszel.ts: Zod-validated Beszel API client with PocketBase auth, system fetching, and MySQL upsert - New api/routes/beszel.ts: GET /api/beszel (read) + POST /api/beszel/collect (trigger) - Updated api/server.ts: register route, add to 5min scheduler - Updated src/pages/Monitor.tsx: Fleet section showing all 17 systems with CPU/RAM/disk bars, status dots, container count, and link to Beszel dashboard Architecture: Beszel hub → API → tbl_eal_monitoring (category='fleet') → Monitor.tsx grid. Collected every 5min alongside existing metrics.
This commit is contained in:
+87
-4
@@ -18,6 +18,7 @@ import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
MonitorDot,
|
||||
Laptop2,
|
||||
} from 'lucide-react'
|
||||
|
||||
// Types
|
||||
@@ -60,6 +61,24 @@ interface VMConfig {
|
||||
accentBg: string
|
||||
accentBorder: string
|
||||
}
|
||||
interface FleetSystem {
|
||||
name: string
|
||||
host: string
|
||||
status: string
|
||||
cpu: number
|
||||
mem: number
|
||||
disk: number
|
||||
containers: number
|
||||
load: number
|
||||
uptime: number
|
||||
}
|
||||
|
||||
interface FleetData {
|
||||
total: number
|
||||
up: number
|
||||
down: number
|
||||
systems: FleetSystem[]
|
||||
}
|
||||
|
||||
// VM definitions matching cluster architecture
|
||||
const VM_CONFIG: Record<string, VMConfig> = {
|
||||
@@ -331,16 +350,25 @@ const VMCard = ({ item, config }: { item?: MonitorItem; config: VMConfig }) => {
|
||||
|
||||
export default function Monitor() {
|
||||
const [data, setData] = useState<MonitorData | null>(null)
|
||||
const [fleet, setFleet] = useState<FleetData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setRefreshing(true)
|
||||
try {
|
||||
const response = await fetch('/api/monitor')
|
||||
if (!response.ok) throw new Error('Failed')
|
||||
const json = await response.json()
|
||||
setData(json)
|
||||
const [monitorRes, fleetRes] = await Promise.all([
|
||||
fetch('/api/monitor'),
|
||||
fetch('/api/beszel'),
|
||||
])
|
||||
if (monitorRes.ok) {
|
||||
const json = await monitorRes.json()
|
||||
setData(json)
|
||||
}
|
||||
if (fleetRes.ok) {
|
||||
const json = await fleetRes.json()
|
||||
setFleet(json)
|
||||
}
|
||||
} catch {
|
||||
if (import.meta.env.DEV) {
|
||||
setData(getMockData())
|
||||
@@ -421,6 +449,61 @@ export default function Monitor() {
|
||||
<VMCard key={key} item={getVMItem(key)} config={config} />
|
||||
))}
|
||||
</div>
|
||||
{/* Section 2.5: Fleet Beszel */}
|
||||
{fleet && fleet.systems.length > 0 && (
|
||||
<motion.div variants={itemVariants} className="mt-8">
|
||||
<div className="glass-card overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-white/5 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-white flex items-center gap-2 uppercase tracking-wide">
|
||||
<Laptop2 className="w-4 h-4 text-cyan-400" />
|
||||
Fleet — {fleet.up}/{fleet.total} activas
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{fleet.down > 0 && (
|
||||
<span className="text-xs bg-red-500/20 text-red-400 px-2 py-1 rounded-full">{fleet.down} down</span>
|
||||
)}
|
||||
<a
|
||||
href="https://descomplicar-beszel.i7zsqo.easypanel.host/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
Abrir Beszel →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
{fleet.systems.sort((a, b) => a.name.localeCompare(b.name)).map((sys) => (
|
||||
<div
|
||||
key={sys.name}
|
||||
className={`p-3 rounded-xl border transition-colors ${
|
||||
sys.status === 'up'
|
||||
? 'bg-white/[0.02] border-white/5 hover:bg-white/[0.04]'
|
||||
: 'bg-red-500/5 border-red-500/10'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<StatusDot status={sys.status} />
|
||||
<span className="text-sm font-medium text-white truncate">{sys.name}</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<ProgressBar percent={sys.cpu} size="sm" />
|
||||
<div className="flex justify-between text-[10px] text-zinc-500">
|
||||
<span>RAM {sys.mem}%</span>
|
||||
<span>DK {sys.disk}%</span>
|
||||
</div>
|
||||
{sys.containers > 0 && (
|
||||
<div className="text-[10px] text-zinc-600">{sys.containers} containers</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Section 3: Detail Categories */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 mt-8">
|
||||
|
||||
Reference in New Issue
Block a user