feat: implement swapper metrics and node cabinet ui

This commit is contained in:
Apple
2025-11-30 15:40:41 -08:00
parent cb9efaf656
commit 281c79f916
5 changed files with 223 additions and 100 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useNodeSwapper, SwapperModel } from '@/hooks/useNodeSwapper';
import { useState } from 'react';
import { useNodeSwapper } from '@/hooks/useNodeSwapper';
interface NodeSwapperCardProps {
nodeId: string;
@@ -9,95 +9,111 @@ interface NodeSwapperCardProps {
export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
const { swapper, isLoading, error, mutate } = useNodeSwapper(nodeId);
const [expanded, setExpanded] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
if (isLoading) {
return (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 animate-pulse">
<div className="h-6 bg-white/10 rounded w-1/3 mb-4" />
<div className="h-4 bg-white/10 rounded w-1/2" />
<div className="h-6 w-32 bg-white/10 rounded mb-4" />
<div className="h-4 w-full bg-white/5 rounded" />
</div>
);
}
if (error) {
return (
<div className="bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20 p-6 text-red-400">
<h3 className="font-semibold flex items-center gap-2">
<span>🧠</span> Swapper Service
</h3>
<p className="text-sm mt-2">Failed to load status</p>
<div className="bg-red-500/10 backdrop-blur-md rounded-2xl border border-red-500/20 p-6">
<h3 className="text-red-400 font-medium mb-2">Swapper Service Unavailable</h3>
<p className="text-white/50 text-sm">Failed to load swapper details.</p>
<button
onClick={() => mutate()}
className="mt-4 text-xs text-white/40 hover:text-white hover:underline"
>
Retry
</button>
</div>
);
}
if (!swapper) return null;
const statusColor = swapper.healthy ? 'text-green-400' : 'text-red-400';
const statusIcon = swapper.healthy ? '🟢' : '🔴';
const statusText = swapper.healthy ? 'Healthy' : 'Down';
return (
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6 transition-all hover:border-cyan-500/30">
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<span>🧠</span> Swapper Service
</h3>
<button
onClick={() => mutate()}
className="text-white/30 hover:text-white/70 transition-colors"
title="Refresh"
>
</button>
<div className="flex items-center gap-2">
<button
onClick={() => mutate()}
className="p-1.5 rounded-lg hover:bg-white/5 text-white/30 hover:text-white transition-colors"
title="Refresh"
>
</button>
</div>
</div>
<div className="space-y-3">
{/* Status Row */}
<div className="flex items-center justify-between text-sm">
<span className="text-white/50">Status:</span>
<span className={`font-medium flex items-center gap-1.5 ${statusColor}`}>
{statusIcon} {statusText}
</span>
<div className="space-y-4">
{/* Status */}
<div className="bg-white/5 rounded-xl p-4 border border-white/5">
<div className="flex items-center justify-between mb-2">
<span className="text-white/50 text-sm">Status</span>
<div className={`flex items-center gap-2 px-2 py-1 rounded-full text-xs font-medium ${
swapper.healthy ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${
swapper.healthy ? 'bg-green-400' : 'bg-red-400'
}`} />
{swapper.healthy ? 'Healthy' : 'Unhealthy'}
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-white/50 text-sm">Models Loaded</span>
<span className="text-white font-mono">
{swapper.models_loaded} <span className="text-white/30">/ {swapper.models_total}</span>
</span>
</div>
</div>
{/* Summary Row */}
<div className="flex items-center justify-between text-sm">
<span className="text-white/50">Models:</span>
<span className="text-white font-medium">
{swapper.models_loaded} / {swapper.models_total} loaded
</span>
</div>
{/* Details Toggle */}
{/* Models List Toggle */}
<button
onClick={() => setExpanded(!expanded)}
className="w-full mt-2 py-2 text-xs font-medium text-white/40 hover:text-white/70 bg-white/5 hover:bg-white/10 rounded-lg transition-colors flex items-center justify-center gap-1"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-4 py-2 rounded-lg bg-white/5 hover:bg-white/10 transition-colors text-sm text-white/70"
>
{expanded ? 'Hide Models' : 'View Models'}
<span>{expanded ? '' : ''}</span>
<span>Available Models</span>
<span className={`transition-transform ${isExpanded ? 'rotate-180' : ''}`}></span>
</button>
{/* Models List */}
{expanded && (
<div className="mt-3 space-y-2 border-t border-white/10 pt-3">
{isExpanded && (
<div className="space-y-2 mt-2">
{swapper.models.length === 0 ? (
<p className="text-white/30 text-xs text-center py-2">No models found</p>
<p className="text-center text-white/30 text-sm py-2">No models found</p>
) : (
swapper.models.map((model, idx) => (
<div key={`${model.name}-${idx}`} className="flex items-center justify-between text-xs bg-black/20 p-2 rounded border border-white/5">
<div className="flex flex-col gap-0.5">
<span className="text-white font-medium">{model.name}</span>
<span className="text-white/40 uppercase text-[10px]">{model.type || 'unknown'}</span>
</div>
<div className="flex flex-col items-end gap-0.5">
<span className={model.loaded ? 'text-green-400' : 'text-white/30'}>
{model.loaded ? '● LOADED' : '○ UNLOADED'}
</span>
{model.vram_gb && (
<span className="text-white/30">{model.vram_gb.toFixed(1)} GB</span>
)}
<div
key={`${model.name}-${idx}`}
className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5"
>
<div className="flex flex-col">
<span className="text-white text-sm font-medium">{model.name}</span>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-white/40 uppercase px-1.5 py-0.5 rounded bg-white/5">
{model.type || 'unknown'}
</span>
{model.vram_gb && (
<span className="text-xs text-white/30">
{model.vram_gb.toFixed(1)} GB VRAM
</span>
)}
</div>
</div>
<div className={`w-2 h-2 rounded-full ${
model.loaded ? 'bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]' : 'bg-white/10'
}`} title={model.loaded ? 'Loaded in VRAM' : 'Not loaded'} />
</div>
))
)}
@@ -107,4 +123,3 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
</div>
);
}

View File

@@ -1,12 +1,23 @@
export { StatusBadge } from './StatusBadge';
export { ProgressBar } from './ProgressBar';
export { NodeSummaryCard } from './NodeSummaryCard';
export { InfraCard } from './InfraCard';
export { AIServicesCard } from './AIServicesCard';
export { AgentsCard } from './AgentsCard';
export { MatrixCard } from './MatrixCard';
export { ModulesCard } from './ModulesCard';
export { NodeStandardComplianceCard } from './NodeStandardComplianceCard';
export { DAGIRouterCard } from './DAGIRouterCard';
export { NodeMetricsCard } from './NodeMetricsCard';
import { NodeSummaryCard } from './NodeSummaryCard';
import { InfraCard } from './InfraCard';
import { AIServicesCard } from './AIServicesCard';
import { AgentsCard } from './AgentsCard';
import { MatrixCard } from './MatrixCard';
import { ModulesCard } from './ModulesCard';
import { NodeStandardComplianceCard } from './NodeStandardComplianceCard';
import { DAGIRouterCard } from './DAGIRouterCard';
import { NodeMetricsCard } from './NodeMetricsCard';
import { NodeSwapperCard } from './NodeSwapperCard';
export {
NodeSummaryCard,
InfraCard,
AIServicesCard,
AgentsCard,
MatrixCard,
ModulesCard,
NodeStandardComplianceCard,
DAGIRouterCard,
NodeMetricsCard,
NodeSwapperCard
};

View File

@@ -1,9 +1,4 @@
import useSWR from "swr";
const fetcher = (url: string) => fetch(url, { credentials: "include" }).then(r => {
if (!r.ok) throw new Error(`Failed to load ${url}`);
return r.json();
});
import useSWR from 'swr';
export interface SwapperModel {
name: string;
@@ -20,7 +15,9 @@ export interface NodeSwapperDetail {
models: SwapperModel[];
}
export function useNodeSwapper(nodeId?: string) {
const fetcher = (url: string) => fetch(url).then((res) => res.json());
export function useNodeSwapper(nodeId: string) {
const { data, error, isLoading, mutate } = useSWR<NodeSwapperDetail>(
nodeId ? `/api/v1/internal/node/${nodeId}/swapper` : null,
fetcher
@@ -33,4 +30,3 @@ export function useNodeSwapper(nodeId?: string) {
mutate,
};
}