Compare commits

...

1 Commits

Author SHA1 Message Date
Alex Cheema
c381ae64ad feat: show download status indicators on topology nodes
Add per-node download status indicators to the topology view for the
currently selected model. Nodes with completed downloads show a green
checkmark; nodes with pending/ongoing downloads show a clickable
download button.

Also extracts shared download parsing utilities into
dashboard/src/lib/utils/downloads.ts and refactors the downloads page
to use them.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 06:07:48 -08:00
4 changed files with 279 additions and 31 deletions

View File

@@ -8,12 +8,16 @@
nodeThunderboltBridge,
type NodeInfo,
} from "$lib/stores/app.svelte";
import { getModelDownloadStatus } from "$lib/utils/downloads";
interface Props {
class?: string;
highlightedNodes?: Set<string>;
filteredNodes?: Set<string>;
onNodeClick?: (nodeId: string) => void;
downloadsData?: Record<string, unknown[]>;
activeModelId?: string | null;
onDownloadToNode?: (nodeId: string) => void;
}
let {
@@ -21,6 +25,9 @@
highlightedNodes = new Set(),
filteredNodes = new Set(),
onNodeClick,
downloadsData,
activeModelId = null,
onDownloadToNode,
}: Props = $props();
let svgContainer: SVGSVGElement | undefined = $state();
@@ -907,6 +914,95 @@
.attr("stroke-width", strokeWidth);
}
// --- Download Status Indicator (top-right of device icon) ---
if (activeModelId && downloadsData) {
const dlStatus = getModelDownloadStatus(
downloadsData,
nodeInfo.id,
activeModelId,
);
if (dlStatus) {
const indicatorSize = isMinimized ? 8 : 12;
const indicatorX =
nodeInfo.x + iconBaseWidth / 2 - indicatorSize * 0.3;
const indicatorY =
nodeInfo.y - iconBaseHeight / 2 - indicatorSize * 0.3;
if (dlStatus === "DownloadCompleted") {
// Green circle with white checkmark
const dlG = nodeG.append("g").attr("class", "download-indicator");
dlG.append("title").text("Downloaded on this node");
dlG
.append("circle")
.attr("cx", indicatorX)
.attr("cy", indicatorY)
.attr("r", indicatorSize)
.attr("fill", "#22c55e")
.attr("stroke", "#15803d")
.attr("stroke-width", 1);
// Checkmark path
const checkScale = indicatorSize / 12;
dlG
.append("path")
.attr(
"d",
`M${indicatorX - 4 * checkScale},${indicatorY} L${indicatorX - 1 * checkScale},${indicatorY + 3.5 * checkScale} L${indicatorX + 5 * checkScale},${indicatorY - 3.5 * checkScale}`,
)
.attr("stroke", "white")
.attr("stroke-width", 2 * checkScale)
.attr("fill", "none")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round");
} else if (onDownloadToNode) {
// Download arrow icon (not completed — pending/ongoing/failed)
const dlG = nodeG
.append("g")
.attr("class", "download-indicator")
.style("cursor", "pointer");
dlG.append("title").text("Download to this node");
dlG
.append("circle")
.attr("cx", indicatorX)
.attr("cy", indicatorY)
.attr("r", indicatorSize)
.attr("fill", "rgba(80, 80, 90, 0.9)")
.attr("stroke", "rgba(255,215,0,0.5)")
.attr("stroke-width", 1);
// Arrow-down path
const arrowScale = indicatorSize / 12;
dlG
.append("path")
.attr(
"d",
`M${indicatorX},${indicatorY - 4 * arrowScale} L${indicatorX},${indicatorY + 1.5 * arrowScale} M${indicatorX - 3 * arrowScale},${indicatorY - 1 * arrowScale} L${indicatorX},${indicatorY + 1.5 * arrowScale} L${indicatorX + 3 * arrowScale},${indicatorY - 1 * arrowScale} M${indicatorX - 4 * arrowScale},${indicatorY + 4 * arrowScale} L${indicatorX + 4 * arrowScale},${indicatorY + 4 * arrowScale}`,
)
.attr("stroke", "rgba(255,215,0,0.8)")
.attr("stroke-width", 1.5 * arrowScale)
.attr("fill", "none")
.attr("stroke-linecap", "round")
.attr("stroke-linejoin", "round");
dlG.on("click", (event: MouseEvent) => {
event.stopPropagation();
onDownloadToNode(nodeInfo.id);
});
dlG
.on("mouseenter", function () {
d3.select(this)
.select("circle")
.attr("stroke", "rgba(255,215,0,1)")
.attr("fill", "rgba(100, 100, 110, 0.9)");
})
.on("mouseleave", function () {
d3.select(this)
.select("circle")
.attr("stroke", "rgba(255,215,0,0.5)")
.attr("fill", "rgba(80, 80, 90, 0.9)");
});
}
}
}
// --- Vertical GPU Bar (right side of icon) ---
// Show in both full mode and minimized mode (scaled appropriately)
if (showFullLabels || isMinimized) {
@@ -1153,6 +1249,8 @@
const _hoveredNodeId = hoveredNodeId;
const _filteredNodes = filteredNodes;
const _highlightedNodes = highlightedNodes;
const _downloadsData = downloadsData;
const _activeModelId = activeModelId;
if (_data) {
renderGraph();
}

View File

@@ -0,0 +1,152 @@
/**
* Shared utilities for parsing and querying download state.
*
* The download state from `/state` is shaped as:
* Record<NodeId, Array<TaggedDownloadEntry>>
*
* Each entry is a tagged union object like:
* { "DownloadCompleted": { shard_metadata: { "PipelineShardMetadata": { model_card: { model_id: "..." }, ... } }, ... } }
*/
/** Unwrap one level of tagged-union envelope, returning [tag, payload]. */
function unwrapTagged(
obj: Record<string, unknown>,
): [string, Record<string, unknown>] | null {
const keys = Object.keys(obj);
if (keys.length !== 1) return null;
const tag = keys[0];
const payload = obj[tag];
if (!payload || typeof payload !== "object") return null;
return [tag, payload as Record<string, unknown>];
}
/** Extract the model ID string from a download entry's nested shard_metadata. */
export function extractModelIdFromDownload(
downloadPayload: Record<string, unknown>,
): string | null {
const shardMetadata =
downloadPayload.shard_metadata ?? downloadPayload.shardMetadata;
if (!shardMetadata || typeof shardMetadata !== "object") return null;
const unwrapped = unwrapTagged(shardMetadata as Record<string, unknown>);
if (!unwrapped) return null;
const [, shardData] = unwrapped;
const modelMeta = shardData.model_card ?? shardData.modelCard;
if (!modelMeta || typeof modelMeta !== "object") return null;
const meta = modelMeta as Record<string, unknown>;
return (meta.model_id as string) ?? (meta.modelId as string) ?? null;
}
/** Extract the shard_metadata object from a download entry payload. */
export function extractShardMetadata(
downloadPayload: Record<string, unknown>,
): Record<string, unknown> | null {
const shardMetadata =
downloadPayload.shard_metadata ?? downloadPayload.shardMetadata;
if (!shardMetadata || typeof shardMetadata !== "object") return null;
return shardMetadata as Record<string, unknown>;
}
/** Get the download tag (DownloadCompleted, DownloadOngoing, etc.) from a wrapped entry. */
export function getDownloadTag(
entry: unknown,
): [string, Record<string, unknown>] | null {
if (!entry || typeof entry !== "object") return null;
return unwrapTagged(entry as Record<string, unknown>);
}
/**
* Iterate over all download entries for a given node, yielding [tag, payload, modelId].
*/
function* iterNodeDownloads(
nodeDownloads: unknown[],
): Generator<[string, Record<string, unknown>, string]> {
for (const entry of nodeDownloads) {
const tagged = getDownloadTag(entry);
if (!tagged) continue;
const [tag, payload] = tagged;
const modelId = extractModelIdFromDownload(payload);
if (!modelId) continue;
yield [tag, payload, modelId];
}
}
/** Check if a specific model is fully downloaded (DownloadCompleted) on a specific node. */
export function isModelDownloadedOnNode(
downloadsData: Record<string, unknown[]>,
nodeId: string,
modelId: string,
): boolean {
const nodeDownloads = downloadsData[nodeId];
if (!Array.isArray(nodeDownloads)) return false;
for (const [tag, , entryModelId] of iterNodeDownloads(nodeDownloads)) {
if (tag === "DownloadCompleted" && entryModelId === modelId) return true;
}
return false;
}
/** Get all node IDs where a model is fully downloaded (DownloadCompleted). */
export function getNodesWithModelDownloaded(
downloadsData: Record<string, unknown[]>,
modelId: string,
): string[] {
const result: string[] = [];
for (const nodeId of Object.keys(downloadsData)) {
if (isModelDownloadedOnNode(downloadsData, nodeId, modelId)) {
result.push(nodeId);
}
}
return result;
}
/**
* Find shard metadata for a model from any download entry across all nodes.
* Returns the first match found (completed entries are preferred).
*/
export function getShardMetadataForModel(
downloadsData: Record<string, unknown[]>,
modelId: string,
): Record<string, unknown> | null {
let fallback: Record<string, unknown> | null = null;
for (const nodeDownloads of Object.values(downloadsData)) {
if (!Array.isArray(nodeDownloads)) continue;
for (const [tag, payload, entryModelId] of iterNodeDownloads(
nodeDownloads,
)) {
if (entryModelId !== modelId) continue;
const shard = extractShardMetadata(payload);
if (!shard) continue;
if (tag === "DownloadCompleted") return shard;
if (!fallback) fallback = shard;
}
}
return fallback;
}
/**
* Get the download status tag for a specific model on a specific node.
* Returns the "best" status: DownloadCompleted > DownloadOngoing > others.
*/
export function getModelDownloadStatus(
downloadsData: Record<string, unknown[]>,
nodeId: string,
modelId: string,
): string | null {
const nodeDownloads = downloadsData[nodeId];
if (!Array.isArray(nodeDownloads)) return null;
let best: string | null = null;
for (const [tag, , entryModelId] of iterNodeDownloads(nodeDownloads)) {
if (entryModelId !== modelId) continue;
if (tag === "DownloadCompleted") return tag;
if (tag === "DownloadOngoing") best = tag;
else if (!best) best = tag;
}
return best;
}

View File

@@ -39,9 +39,11 @@
toggleChatSidebarVisible,
thunderboltBridgeCycles,
nodeThunderboltBridge,
startDownload,
type DownloadProgress,
type PlacementPreview,
} from "$lib/stores/app.svelte";
import { getShardMetadataForModel } from "$lib/utils/downloads";
import HeaderNav from "$lib/components/HeaderNav.svelte";
import { fade, fly } from "svelte/transition";
import { cubicInOut } from "svelte/easing";
@@ -584,6 +586,17 @@
isModelPickerOpen = false;
}
async function handleTopologyDownload(nodeId: string) {
if (!selectedModelId) return;
const shardMeta = getShardMetadataForModel(
downloadsData ?? {},
selectedModelId,
);
if (shardMeta) {
await startDownload(nodeId, shardMeta);
}
}
async function launchInstance(
modelId: string,
specificPreview?: PlacementPreview | null,
@@ -1722,6 +1735,9 @@
highlightedNodes={highlightedNodes()}
filteredNodes={nodeFilter}
onNodeClick={togglePreviewNodeFilter}
{downloadsData}
activeModelId={selectedModelId}
onDownloadToNode={handleTopologyDownload}
/>
<!-- Thunderbolt Bridge Cycle Warning -->
@@ -2771,6 +2787,9 @@
highlightedNodes={highlightedNodes()}
filteredNodes={nodeFilter}
onNodeClick={togglePreviewNodeFilter}
{downloadsData}
activeModelId={selectedModelId}
onDownloadToNode={handleTopologyDownload}
/>
<!-- Thunderbolt Bridge Cycle Warning (compact) -->

View File

@@ -10,6 +10,11 @@
deleteDownload,
} from "$lib/stores/app.svelte";
import HeaderNav from "$lib/components/HeaderNav.svelte";
import {
extractModelIdFromDownload,
extractShardMetadata,
getDownloadTag,
} from "$lib/utils/downloads";
type FileProgress = {
name: string;
@@ -98,26 +103,7 @@
return Math.min(100, Math.max(0, value as number));
}
function extractModelIdFromDownload(
downloadPayload: Record<string, unknown>,
): string | null {
const shardMetadata =
downloadPayload.shard_metadata ?? downloadPayload.shardMetadata;
if (!shardMetadata || typeof shardMetadata !== "object") return null;
const shardObj = shardMetadata as Record<string, unknown>;
const shardKeys = Object.keys(shardObj);
if (shardKeys.length !== 1) return null;
const shardData = shardObj[shardKeys[0]] as Record<string, unknown>;
if (!shardData) return null;
const modelMeta = shardData.model_card ?? shardData.modelCard;
if (!modelMeta || typeof modelMeta !== "object") return null;
const meta = modelMeta as Record<string, unknown>;
return (meta.model_id as string) ?? (meta.modelId as string) ?? null;
}
// extractModelIdFromDownload imported from $lib/utils/downloads
function parseDownloadProgress(
payload: Record<string, unknown>,
@@ -197,14 +183,10 @@
for (const downloadWrapped of nodeEntries) {
if (!downloadWrapped || typeof downloadWrapped !== "object") continue;
const keys = Object.keys(downloadWrapped as Record<string, unknown>);
if (keys.length !== 1) continue;
const tagged = getDownloadTag(downloadWrapped);
if (!tagged) continue;
const downloadKind = keys[0];
const downloadPayload = (downloadWrapped as Record<string, unknown>)[
downloadKind
] as Record<string, unknown>;
if (!downloadPayload) continue;
const [downloadKind, downloadPayload] = tagged;
const modelId =
extractModelIdFromDownload(downloadPayload) ?? "unknown-model";
@@ -273,10 +255,7 @@
}
// Extract shard_metadata for use with download actions
const shardMetadata = (downloadPayload.shard_metadata ??
downloadPayload.shardMetadata) as
| Record<string, unknown>
| undefined;
const shardMetadata = extractShardMetadata(downloadPayload);
const entry: ModelEntry = {
modelId,