Bump version to 1.0.51

This commit is contained in:
2026-07-19 19:49:00 -04:00
parent 4ccf2fc2d6
commit 6b29a4b419
11 changed files with 454 additions and 27 deletions
+131
View File
@@ -28,18 +28,32 @@ import {
Terminal,
AlertCircle,
Ban,
ChevronDown,
Activity,
} from 'lucide-react';
import { toast } from 'sonner';
import { statusVariant, statusLabel, getErrorCodeInfo } from '@/lib/status';
import { formatDuration } from '@/lib/utils';
import { cn } from '@/lib/utils';
interface SSEProgress {
fileBytes: number;
pct: number;
speedBps: number;
etaSeconds: number;
xfrDone: number;
xfrTotal: number;
}
interface SSEEvent {
type: string;
job_id: number;
status?: string;
line?: string;
stream?: string;
progress?: SSEProgress;
totalBytes?: number;
sentBytes?: number;
}
export default function JobDetail() {
@@ -56,6 +70,10 @@ export default function JobDetail() {
const [cancelModal, setCancelModal] = useState(false);
const [cancelReason, setCancelReason] = useState('');
const [errorModal, setErrorModal] = useState(false);
const [progress, setProgress] = useState<SSEProgress | null>(null);
const [finalTotals, setFinalTotals] = useState<{ totalBytes: number; sentBytes: number } | null>(null);
const [hasMoreLogs, setHasMoreLogs] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
useEffect(() => {
loadJob();
@@ -78,6 +96,13 @@ export default function JobDetail() {
return updated;
});
}
if (evt.type === 'progress' && evt.progress) {
setProgress(evt.progress);
}
if (evt.type === 'progress_total' && evt.totalBytes !== undefined && evt.sentBytes !== undefined) {
setFinalTotals({ totalBytes: evt.totalBytes, sentBytes: evt.sentBytes });
setProgress(null);
}
};
}
return () => esRef.current?.close();
@@ -109,8 +134,10 @@ export default function JobDetail() {
)) ?? [];
if (offset === 0) {
setLogs(ls);
setHasMoreLogs(ls.length === 1000);
} else {
setLogs(prev => [...prev, ...ls]);
setHasMoreLogs(ls.length === 1000);
}
} catch {}
}
@@ -321,6 +348,10 @@ export default function JobDetail() {
</div>
)}
{(progress || finalTotals) && (
<TransferProgress progress={progress} finalTotals={finalTotals} />
)}
<Card className="flex flex-col min-h-0">
<div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
<div className="flex items-center gap-2">
@@ -378,6 +409,23 @@ export default function JobDetail() {
</>
)}
<div ref={logEndRef} />
{hasMoreLogs && (
<div className="flex justify-center py-2">
<Button
variant="secondary"
size="sm"
onClick={async () => {
setLoadingMore(true);
await loadLogs(logs.length);
setLoadingMore(false);
}}
disabled={loadingMore}
>
<ChevronDown className="h-4 w-4" />
{loadingMore ? 'Loading...' : 'Load more'}
</Button>
</div>
)}
</div>
</Card>
@@ -452,3 +500,86 @@ function LogLine({
</div>
);
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'kB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`;
}
function formatSpeed(bps: number): string {
if (bps === 0) return '0 B/s';
const units = ['B/s', 'kB/s', 'MB/s', 'GB/s'];
const i = Math.floor(Math.log(bps) / Math.log(1000));
return `${(bps / Math.pow(1000, i)).toFixed(1)} ${units[i]}`;
}
function TransferProgress({
progress,
finalTotals,
}: {
progress: SSEProgress | null;
finalTotals: { totalBytes: number; sentBytes: number } | null;
}) {
const globalPct = finalTotals && finalTotals.totalBytes > 0
? Math.round((finalTotals.sentBytes / finalTotals.totalBytes) * 100)
: null;
return (
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-accent" />
<span className="text-xs font-semibold text-fg-muted uppercase tracking-wider">
Transfer Progress
</span>
</div>
{progress && (
<>
<div className="space-y-1.5">
<div className="flex justify-between text-xs text-fg-muted">
<span>Current file</span>
<span>{progress.pct}%</span>
</div>
<div className="h-2 bg-border rounded-full overflow-hidden">
<div
className="h-full bg-accent transition-all duration-300 rounded-full"
style={{ width: `${progress.pct}%` }}
/>
</div>
</div>
<div className="flex justify-between text-xs text-fg-muted">
<span className="font-mono">
xfr#{(progress.xfrDone).toLocaleString()}/{progress.xfrTotal > 0 ? progress.xfrTotal.toLocaleString() : '?'}
</span>
<span className="font-mono">
{formatSpeed(progress.speedBps)}
</span>
<span className="font-mono">
ETA {progress.etaSeconds > 0 ? `${Math.floor(progress.etaSeconds / 60)}m ${progress.etaSeconds % 60}s` : '-'}
</span>
</div>
</>
)}
{finalTotals && (
<div className="space-y-1.5">
<div className="flex justify-between text-xs text-fg-muted">
<span>Total transferred</span>
<span>{globalPct}% {formatBytes(finalTotals.sentBytes)} / {formatBytes(finalTotals.totalBytes)}</span>
</div>
<div className="h-2 bg-border rounded-full overflow-hidden">
<div
className="h-full bg-emerald-500 transition-all duration-300 rounded-full"
style={{ width: `${globalPct ?? 0}%` }}
/>
</div>
</div>
)}
</div>
</Card>
);
}