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) {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user