From 394dd62980b04a5ef388bfc1ae31b5abe9cc448c Mon Sep 17 00:00:00 2001 From: Apple Date: Mon, 1 Dec 2025 02:18:00 -0800 Subject: [PATCH] feat: add swapper api proxy and improved card --- .../internal/node/[nodeId]/swapper/route.ts | 44 +++++++++++++++++++ .../node-dashboard/NodeSwapperCard.tsx | 29 ++++++++---- apps/web/src/hooks/useNodeSwapper.ts | 11 ++++- 3 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/app/api/internal/node/[nodeId]/swapper/route.ts diff --git a/apps/web/src/app/api/internal/node/[nodeId]/swapper/route.ts b/apps/web/src/app/api/internal/node/[nodeId]/swapper/route.ts new file mode 100644 index 00000000..a5664a8c --- /dev/null +++ b/apps/web/src/app/api/internal/node/[nodeId]/swapper/route.ts @@ -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 } + ); + } +} + diff --git a/apps/web/src/components/node-dashboard/NodeSwapperCard.tsx b/apps/web/src/components/node-dashboard/NodeSwapperCard.tsx index 99710ceb..64156eda 100644 --- a/apps/web/src/components/node-dashboard/NodeSwapperCard.tsx +++ b/apps/web/src/components/node-dashboard/NodeSwapperCard.tsx @@ -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 (
@@ -60,19 +65,19 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) {
Status
- {swapper.healthy ? 'Healthy' : 'Unhealthy'} + {summary.healthy ? 'Healthy' : hasData ? 'Degraded' : 'Pending data'}
Models Loaded - {swapper.models_loaded} / {swapper.models_total} + {summary.models_loaded} / {summary.models_total}
@@ -87,12 +92,12 @@ export function NodeSwapperCard({ nodeId }: NodeSwapperCardProps) { {/* Models List */} - {isExpanded && ( + {isExpanded && hasData && (
- {swapper.models.length === 0 ? ( + {summary.models.length === 0 ? (

No models found

) : ( - swapper.models.map((model, idx) => ( + summary.models.map((model, idx) => (
)} + + {!hasData && !error && ( +
+ Дані Swapper ще не надійшли. Як тільки node-guardian оновить heartbeat, тут з’являться моделі. +
+ )}
); diff --git a/apps/web/src/hooks/useNodeSwapper.ts b/apps/web/src/hooks/useNodeSwapper.ts index e2dd4018..413b86b1 100644 --- a/apps/web/src/hooks/useNodeSwapper.ts +++ b/apps/web/src/hooks/useNodeSwapper.ts @@ -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( - nodeId ? `/api/v1/internal/node/${nodeId}/swapper` : null, + nodeId ? `/api/internal/node/${nodeId}/swapper` : null, fetcher );