feat: add swapper api proxy and improved card

This commit is contained in:
Apple
2025-12-01 02:18:00 -08:00
parent f0d113e234
commit 394dd62980
3 changed files with 73 additions and 11 deletions

View 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 }
);
}
}

View File

@@ -10,6 +10,13 @@ interface NodeSwapperCardProps {
export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
const { swapper, isLoading, error, mutate } = useNodeSwapper(nodeId);
const [isExpanded, setIsExpanded] = useState(false);
const hasData = Boolean(swapper);
const summary = swapper ?? {
healthy: false,
models_loaded: 0,
models_total: 0,
models: [],
};
if (isLoading) {
return (
@@ -35,8 +42,6 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
);
}
if (!swapper) return null;
return (
<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">
@@ -60,19 +65,19 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
<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'
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 ${
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 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>
{summary.models_loaded} <span className="text-white/30">/ {summary.models_total}</span>
</span>
</div>
</div>
@@ -87,12 +92,12 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
</button>
{/* Models List */}
{isExpanded && (
{isExpanded && hasData && (
<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>
) : (
swapper.models.map((model, idx) => (
summary.models.map((model, idx) => (
<div
key={`${model.name}-${idx}`}
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>
)}
{!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>
);

View File

@@ -15,11 +15,18 @@ export interface NodeSwapperDetail {
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) {
const { data, error, isLoading, mutate } = useSWR<NodeSwapperDetail>(
nodeId ? `/api/v1/internal/node/${nodeId}/swapper` : null,
nodeId ? `/api/internal/node/${nodeId}/swapper` : null,
fetcher
);