feat: add swapper api proxy and improved card
This commit is contained in:
44
apps/web/src/app/api/internal/node/[nodeId]/swapper/route.ts
Normal file
44
apps/web/src/app/api/internal/node/[nodeId]/swapper/route.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
const CITY_SERVICE_URL =
|
||||||
|
process.env.INTERNAL_API_URL ||
|
||||||
|
process.env.CITY_SERVICE_URL ||
|
||||||
|
'http://daarion-city-service:7001';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
_request: Request,
|
||||||
|
{ params }: { params: { nodeId: string } }
|
||||||
|
) {
|
||||||
|
const nodeId = params?.nodeId;
|
||||||
|
|
||||||
|
if (!nodeId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'nodeId is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const upstream = await fetch(
|
||||||
|
`${CITY_SERVICE_URL}/city/internal/node/${encodeURIComponent(nodeId)}/swapper`,
|
||||||
|
{
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const payload = await upstream.json();
|
||||||
|
return NextResponse.json(payload, { status: upstream.status });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Failed to fetch swapper data',
|
||||||
|
details: error instanceof Error ? error.message : String(error),
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -10,6 +10,13 @@ interface NodeSwapperCardProps {
|
|||||||
export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
||||||
const { swapper, isLoading, error, mutate } = useNodeSwapper(nodeId);
|
const { swapper, isLoading, error, mutate } = useNodeSwapper(nodeId);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const hasData = Boolean(swapper);
|
||||||
|
const summary = swapper ?? {
|
||||||
|
healthy: false,
|
||||||
|
models_loaded: 0,
|
||||||
|
models_total: 0,
|
||||||
|
models: [],
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -35,8 +42,6 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!swapper) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
|
<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">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -60,19 +65,19 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
|||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-white/50 text-sm">Status</span>
|
<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 ${
|
<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'
|
summary.healthy ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-300'
|
||||||
}`}>
|
}`}>
|
||||||
<span className={`w-1.5 h-1.5 rounded-full ${
|
<span className={`w-1.5 h-1.5 rounded-full ${
|
||||||
swapper.healthy ? 'bg-green-400' : 'bg-red-400'
|
summary.healthy ? 'bg-green-400' : 'bg-yellow-300'
|
||||||
}`} />
|
}`} />
|
||||||
{swapper.healthy ? 'Healthy' : 'Unhealthy'}
|
{summary.healthy ? 'Healthy' : hasData ? 'Degraded' : 'Pending data'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-white/50 text-sm">Models Loaded</span>
|
<span className="text-white/50 text-sm">Models Loaded</span>
|
||||||
<span className="text-white font-mono">
|
<span className="text-white font-mono">
|
||||||
{swapper.models_loaded} <span className="text-white/30">/ {swapper.models_total}</span>
|
{summary.models_loaded} <span className="text-white/30">/ {summary.models_total}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,12 +92,12 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Models List */}
|
{/* Models List */}
|
||||||
{isExpanded && (
|
{isExpanded && hasData && (
|
||||||
<div className="space-y-2 mt-2">
|
<div className="space-y-2 mt-2">
|
||||||
{swapper.models.length === 0 ? (
|
{summary.models.length === 0 ? (
|
||||||
<p className="text-center text-white/30 text-sm 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) => (
|
summary.models.map((model, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`${model.name}-${idx}`}
|
key={`${model.name}-${idx}`}
|
||||||
className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5"
|
className="flex items-center justify-between p-3 rounded-lg bg-white/5 border border-white/5"
|
||||||
@@ -119,6 +124,12 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!hasData && !error && (
|
||||||
|
<div className="mt-3 text-xs text-white/40 bg-white/5 rounded-lg p-3 border border-white/5">
|
||||||
|
Дані Swapper ще не надійшли. Як тільки node-guardian оновить heartbeat, тут з’являться моделі.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,11 +15,18 @@ export interface NodeSwapperDetail {
|
|||||||
models: SwapperModel[];
|
models: SwapperModel[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
async function fetcher(url: string) {
|
||||||
|
const res = await fetch(url, { cache: 'no-store' });
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(error?.error || 'Failed to load swapper data');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
export function useNodeSwapper(nodeId: string) {
|
export function useNodeSwapper(nodeId: string) {
|
||||||
const { data, error, isLoading, mutate } = useSWR<NodeSwapperDetail>(
|
const { data, error, isLoading, mutate } = useSWR<NodeSwapperDetail>(
|
||||||
nodeId ? `/api/v1/internal/node/${nodeId}/swapper` : null,
|
nodeId ? `/api/internal/node/${nodeId}/swapper` : null,
|
||||||
fetcher
|
fetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user