fix(dashboard): keep VPS creation progress reliable
Build and publish Docker image / build-and-publish (push) Successful in 35s

This commit is contained in:
wfalkowski committed 2026-10-20 19:03:15 +00:00
1 parent 179a8c274b
commit 008a3a4233
1 file changed
+119 -36
+119 -36
View File
@@ -502,7 +502,9 @@ function DashboardPage({ setIsLoginOpen, setIsRegisterOpen, onCurrencyChange, pr
visible: false, visible: false,
phase: '', phase: '',
progress: 0, progress: 0,
message: '' message: '',
serverType: '',
state: 'active'
}); });
const [personalInfo, setPersonalInfo] = useState({ const [personalInfo, setPersonalInfo] = useState({
firstName: user?.first_name || '', firstName: user?.first_name || '',
@@ -973,11 +975,21 @@ const handleWebSocketMessage = useCallback((data) => {
break; break;
case 'server_created': case 'server_created':
// Hide progress notification when server creation completes setServerCreationProgress({
setServerCreationProgress(prev => ({ ...prev, visible: false })); visible: true,
phase: 'Provisioned',
progress: 100,
message: data.server?.type === 'vps'
? 'The VPS has been created. It may take a little longer to boot.'
: 'Your server has been created.',
serverType: data.server?.type || '',
state: 'complete'
});
if (data.server && data.server.id) { if (data.server && data.server.id) {
setServers(prev => [...prev, data.server]); setServers(prev => prev.some(server => server.id === data.server.id)
? prev.map(server => server.id === data.server.id ? data.server : server)
: [...prev, data.server]);
setIsCreatingServer(false); setIsCreatingServer(false);
} else if (data.serverId) { } else if (data.serverId) {
sendMessageRef.current?.({ sendMessageRef.current?.({
@@ -985,6 +997,7 @@ const handleWebSocketMessage = useCallback((data) => {
}); });
setIsCreatingServer(false); setIsCreatingServer(false);
} }
setIsCreatingServer(false);
if (data.credits !== undefined) { if (data.credits !== undefined) {
setUserCredits(data.credits); setUserCredits(data.credits);
@@ -1124,24 +1137,49 @@ const handleWebSocketMessage = useCallback((data) => {
break; break;
case 'server_creation_progress': case 'server_creation_progress':
// Hide progress if no active creation found (after reconnection check) if (data.state === 'unknown' || data.phase === 'Unknown') {
if (data.phase === 'Unknown' && data.progress === 0 && data.message === 'No active server creation found') { setServerCreationProgress(prev => prev.visible && prev.state === 'active'
setServerCreationProgress(prev => ({ ...prev, visible: false })); ? {
...prev,
phase: 'Status unavailable',
message: 'The connection lost the latest update. Check your server list for the current status.',
state: 'unknown'
}
: prev);
break; break;
} }
setServerCreationProgress({ setServerCreationProgress(prev => ({
...prev,
visible: true, visible: true,
phase: data.phase || '', phase: data.phase || prev.phase,
progress: data.progress || 0, progress: Math.max(prev.progress || 0, Math.min(100, Math.max(0, Number(data.progress) || 0))),
message: data.message || 'Creating server...' message: data.message || prev.message || 'Creating server…',
}); serverType: data.serverType || prev.serverType,
// Hide notification when progress reaches 100% state: data.state || (Number(data.progress) >= 100 ? 'complete' : 'active')
if (data.progress >= 100) { }));
setTimeout(() => { break;
setServerCreationProgress(prev => ({ ...prev, visible: false }));
}, 2000); case 'server_creation_failed':
} setIsCreatingServer(false);
setServerCreationProgress(prev => ({
...prev,
visible: true,
phase: 'Failed',
message: data.error || 'Server creation failed. Check the error details and try again.',
state: 'failed'
}));
break;
case 'capacity_error':
setIsCreatingServer(false);
setServerCreationProgress(prev => ({
...prev,
visible: true,
phase: 'Capacity check',
message: data.error || 'There is not enough capacity for this server right now.',
state: 'failed'
}));
break; break;
case 'port_exposed': case 'port_exposed':
@@ -1247,6 +1285,14 @@ const handleWebSocketMessage = useCallback((data) => {
} }
}, [dashboardStatus, serverCreationProgress.visible, sendDashboardMessage]); }, [dashboardStatus, serverCreationProgress.visible, sendDashboardMessage]);
useEffect(() => {
if (serverCreationProgress.state !== 'complete') return undefined;
const timeoutId = setTimeout(() => {
setServerCreationProgress(prev => ({ ...prev, visible: false }));
}, 4000);
return () => clearTimeout(timeoutId);
}, [serverCreationProgress.state]);
// Request metrics when server is selected and periodically update // Request metrics when server is selected and periodically update
useEffect(() => { useEffect(() => {
if (!selectedServer || !sendDashboardMessage || dashboardStatus !== 'connected') { if (!selectedServer || !sendDashboardMessage || dashboardStatus !== 'connected') {
@@ -1325,6 +1371,16 @@ const handleWebSocketMessage = useCallback((data) => {
const handleCreateServer = async (serverConfig) => { const handleCreateServer = async (serverConfig) => {
setIsCreatingServer(true); setIsCreatingServer(true);
setServerCreationProgress({
visible: true,
phase: 'Preparing',
progress: 1,
message: serverConfig.type === 'vps'
? 'Checking capacity and preparing your VPS.'
: 'Checking capacity and preparing your server.',
serverType: serverConfig.type,
state: 'active'
});
sendDashboardMessage({ sendDashboardMessage({
type: 'create_server', type: 'create_server',
name: serverConfig.name, name: serverConfig.name,
@@ -4458,7 +4514,7 @@ const handleWebSocketMessage = useCallback((data) => {
borderColor: colors.brightBlack borderColor: colors.brightBlack
}} }}
> >
<div className="p-4"> <div className="p-4" role="status" aria-live="polite" aria-atomic="true">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<div className="p-2 rounded-lg" style={{ backgroundColor: `${colors.blue}20` }}> <div className="p-2 rounded-lg" style={{ backgroundColor: `${colors.blue}20` }}>
@@ -4466,32 +4522,56 @@ const handleWebSocketMessage = useCallback((data) => {
</div> </div>
<div> <div>
<h4 className="text-sm font-semibold" style={{ color: colors.brightWhite }}> <h4 className="text-sm font-semibold" style={{ color: colors.brightWhite }}>
Server Creation in Progress {serverCreationProgress.state === 'failed'
? 'Server creation failed'
: serverCreationProgress.state === 'complete'
? 'Server created'
: serverCreationProgress.state === 'unknown'
? 'Creation status unavailable'
: serverCreationProgress.serverType === 'vps'
? 'VPS creation'
: 'Server creation'}
</h4> </h4>
<p className="text-xs mt-0.5" style={{ color: colors.foreground }}> <p className="text-xs mt-0.5" style={{ color: colors.foreground }}>
{serverCreationProgress.message} {serverCreationProgress.message}
</p> </p>
</div> </div>
</div> </div>
<button {serverCreationProgress.state !== 'active' && (
onClick={() => setServerCreationProgress(prev => ({ ...prev, visible: false }))} <button
className="p-1 rounded-lg transition-all duration-200 hover:scale-105" onClick={() => setServerCreationProgress(prev => ({ ...prev, visible: false }))}
style={{ className="p-1 rounded-lg border"
backgroundColor: colors.brightBlack, style={{
color: colors.foreground backgroundColor: colors.brightBlack,
}} borderColor: colors.foreground,
> color: colors.brightWhite
<X className="w-4 h-4" /> }}
</button> aria-label="Dismiss server creation status"
>
<X className="w-4 h-4" />
</button>
)}
</div> </div>
{/* Progress Bar */} {/* Progress Bar */}
<div className="w-full h-2 rounded-full overflow-hidden" style={{ backgroundColor: colors.brightBlack }}> <div
className="w-full h-2 border overflow-hidden"
style={{ backgroundColor: colors.brightBlack, borderColor: colors.foreground }}
role="progressbar"
aria-label="Server creation progress"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(serverCreationProgress.progress)}
>
<div <div
className="h-full rounded-full transition-all duration-300 ease-out" className="h-full transition-[width] duration-500 ease-out"
style={{ style={{
width: `${serverCreationProgress.progress}%`, width: `${Math.min(100, Math.max(0, serverCreationProgress.progress))}%`,
backgroundColor: colors.blue backgroundColor: serverCreationProgress.state === 'failed'
? colors.red
: serverCreationProgress.state === 'complete'
? colors.green
: colors.blue
}} }}
/> />
</div> </div>
@@ -4501,8 +4581,11 @@ const handleWebSocketMessage = useCallback((data) => {
<span className="text-xs" style={{ color: colors.foreground }}> <span className="text-xs" style={{ color: colors.foreground }}>
{serverCreationProgress.phase} {serverCreationProgress.phase}
</span> </span>
<span className="text-xs font-medium" style={{ color: colors.blue }}> <span
{serverCreationProgress.progress.toFixed(1)}% className="text-xs font-bold tabular-nums"
style={{ color: serverCreationProgress.state === 'failed' ? colors.red : colors.blue }}
>
{Math.round(serverCreationProgress.progress)}%
</span> </span>
</div> </div>
</div> </div>