diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt index 9902f178..f04c001f 100644 --- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt +++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt @@ -1 +1 @@ -28 +29 diff --git a/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx b/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx index 2e0299cb..5079f08c 100644 --- a/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx +++ b/src/Web/autopilot-monitor-web/components/DownloadProgress.tsx @@ -18,6 +18,7 @@ interface DownloadItem { bytesTotal: number; downloadRateBps: number; lastUpdated: string; + lastUpdatedMs: number; isComplete: boolean; firstSeenIndex: number; eventData?: Record; @@ -39,22 +40,18 @@ function formatSpeed(bps: number): string { return `${(bps / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`; } -function formatEta(bytesRemaining: number, rateBps: number): string { - if (rateBps <= 0 || bytesRemaining <= 0) return "--"; - const seconds = bytesRemaining / rateBps; - if (seconds < 60) return `${Math.round(seconds)}s`; - if (seconds < 3600) return `${Math.round(seconds / 60)}m ${Math.round(seconds % 60)}s`; - return `${Math.floor(seconds / 3600)}h ${Math.round((seconds % 3600) / 60)}m`; -} - export default function DownloadProgress({ events }: DownloadProgressProps) { const downloads = useMemo(() => { if (events.length === 0) return []; + const sortedEvents = [...events].sort( + (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() + ); + const downloadMap = new Map(); let insertionIndex = 0; - for (const evt of events) { + for (const evt of sortedEvents) { const d = evt.data; if (!d) continue; @@ -65,9 +62,10 @@ export default function DownloadProgress({ events }: DownloadProgressProps) { const bytesDownloaded = parseInt(d.bytes_downloaded ?? d.bytesDownloaded ?? "0", 10); const bytesTotal = parseInt(d.bytes_total ?? d.bytesTotal ?? "0", 10); - const downloadRateBps = parseFloat(d.download_rate_bps ?? d.downloadRateBps ?? "0"); + const reportedRateBps = parseFloat(d.download_rate_bps ?? d.downloadRateBps ?? "0"); const status = d.status ?? ""; const isDownloadStartEvent = evt.eventType === "app_download_started"; + const eventTs = new Date(evt.timestamp).getTime(); // Determine if complete: explicit status or bytes comparison const isComplete = status === "completed" || status === "failed" || (bytesTotal > 0 && bytesDownloaded >= bytesTotal); @@ -90,12 +88,24 @@ export default function DownloadProgress({ events }: DownloadProgressProps) { } const existing = downloadMap.get(appName); + let effectiveRateBps = isNaN(reportedRateBps) ? 0 : reportedRateBps; + if (effectiveRateBps <= 0 && existing && Number.isFinite(eventTs) && Number.isFinite(existing.lastUpdatedMs)) { + const elapsedSeconds = (eventTs - existing.lastUpdatedMs) / 1000; + const deltaBytes = (isNaN(bytesDownloaded) ? 0 : bytesDownloaded) - existing.bytesDownloaded; + if (elapsedSeconds > 0 && deltaBytes > 0) { + effectiveRateBps = deltaBytes / elapsedSeconds; + } else { + effectiveRateBps = existing.downloadRateBps; + } + } + downloadMap.set(appName, { appName, bytesDownloaded: isNaN(bytesDownloaded) ? 0 : bytesDownloaded, bytesTotal: isNaN(bytesTotal) ? 0 : bytesTotal, - downloadRateBps: isNaN(downloadRateBps) ? 0 : downloadRateBps, + downloadRateBps: effectiveRateBps, lastUpdated: evt.timestamp, + lastUpdatedMs: Number.isFinite(eventTs) ? eventTs : (existing?.lastUpdatedMs ?? Date.now()), isComplete, firstSeenIndex: existing?.firstSeenIndex ?? insertionIndex++, eventData: d, @@ -149,17 +159,18 @@ export default function DownloadProgress({ events }: DownloadProgressProps) { const progressPercent = dl.bytesTotal > 0 ? Math.min(100, (dl.bytesDownloaded / dl.bytesTotal) * 100) : 0; - const bytesRemaining = dl.bytesTotal > 0 ? dl.bytesTotal - dl.bytesDownloaded : 0; - return ; + return ; })} } ); } -function DownloadItem({ download: dl, progressPercent, bytesRemaining }: { download: DownloadItem; progressPercent: number; bytesRemaining: number }) { +function DownloadItem({ download: dl, progressPercent }: { download: DownloadItem; progressPercent: number }) { const [showDetails, setShowDetails] = useState(false); + const hasKnownTotal = dl.bytesTotal > 0; + const showProgressBar = hasKnownTotal || !dl.isComplete; return (
0 && ( {formatSpeed(dl.downloadRateBps)} )} - {!dl.isComplete && dl.bytesTotal > 0 && ( - ETA: {formatEta(bytesRemaining, dl.downloadRateBps)} - )} {dl.eventData && Object.keys(dl.eventData).length > 0 && (
{/* Progress bar */} - {dl.bytesTotal > 0 && ( + {showProgressBar && (
- {formatBytes(dl.bytesDownloaded)} / {formatBytes(dl.bytesTotal)} - {progressPercent.toFixed(0)}% + + {hasKnownTotal + ? `${formatBytes(dl.bytesDownloaded)} / ${formatBytes(dl.bytesTotal)}` + : `${formatBytes(dl.bytesDownloaded)} downloaded`} + + {hasKnownTotal ? `${progressPercent.toFixed(0)}%` : "started"}
)}