@@ -17,17 +17,20 @@ import {
1717 KeyRound ,
1818 MapPin ,
1919 RefreshCw ,
20+ RotateCw ,
2021 Trash ,
2122 AlertTriangle ,
23+ Loader2 ,
2224 Package ,
2325} from 'lucide-react' ;
2426import { SiteHeader } from '@/components/site-header' ;
2527import { EnvironmentBadge } from '@/components/environment-badge' ;
28+ import { EnvironmentStatusBadge } from '@/components/environment-status-badge' ;
2629import { Card } from '@/components/ui/card' ;
2730import { Button } from '@/components/ui/button' ;
2831import { Badge } from '@/components/ui/badge' ;
2932import { Separator } from '@/components/ui/separator' ;
30- import { useEnvironmentDetail } from '@/hooks/useEnvironments' ;
33+ import { useEnvironmentDetail , useRetryProvisioning } from '@/hooks/useEnvironments' ;
3134import { useClient } from '@objectstack/client-react' ;
3235import { useProductionGuard } from '@/components/production-guard' ;
3336import { toast } from '@/hooks/use-toast' ;
@@ -36,13 +39,47 @@ function EnvironmentOverviewComponent() {
3639 const { environmentId } = useParams ( {
3740 from : '/environments/$environmentId' ,
3841 } ) ;
39- const { detail, loading } = useEnvironmentDetail ( environmentId ) ;
42+ const { detail, loading, reload } = useEnvironmentDetail ( environmentId ) ;
4043 const client = useClient ( ) as any ;
4144 const navigate = useNavigate ( ) ;
4245 const guard = useProductionGuard ( ) ;
4346 const [ rotating , setRotating ] = useState ( false ) ;
47+ const { retry, retrying } = useRetryProvisioning ( ) ;
4448
4549 const env = detail ?. environment ;
50+ const provisioningError =
51+ ( env ?. metadata as Record < string , any > | undefined ) ?. provisioningError as
52+ | { message ?: string ; failedAt ?: string }
53+ | undefined ;
54+
55+ const handleRetry = async ( ) => {
56+ if ( ! env ) return ;
57+ try {
58+ const result = await retry ( env . id ) ;
59+ const nextStatus = ( result as any ) ?. environment ?. status ;
60+ if ( nextStatus === 'active' ) {
61+ toast ( {
62+ title : 'Provisioning complete' ,
63+ description : 'The environment is now active and ready to use.' ,
64+ } ) ;
65+ } else if ( nextStatus === 'failed' ) {
66+ toast ( {
67+ title : 'Retry failed' ,
68+ description :
69+ ( result as any ) ?. environment ?. metadata ?. provisioningError ?. message ??
70+ 'Provisioning failed again. Check server logs.' ,
71+ variant : 'destructive' ,
72+ } ) ;
73+ }
74+ await reload ( ) ;
75+ } catch ( err ) {
76+ toast ( {
77+ title : 'Retry failed' ,
78+ description : ( err as Error ) . message ,
79+ variant : 'destructive' ,
80+ } ) ;
81+ }
82+ } ;
4683
4784 const handleRotate = async ( ) => {
4885 if ( ! env ) return ;
@@ -100,20 +137,32 @@ function EnvironmentOverviewComponent() {
100137 { env . isDefault && (
101138 < Badge variant = "outline" > default</ Badge >
102139 ) }
103- < Badge variant = "secondary" > { env . status } </ Badge >
140+ < EnvironmentStatusBadge status = { env . status } / >
104141 </ div >
105142 < p className = "mt-1 font-mono text-xs text-muted-foreground" >
106143 { env . id }
107144 </ p >
108145 </ div >
109146 < div className = "flex gap-2" >
147+ < Button
148+ variant = "outline"
149+ size = "sm"
150+ onClick = { ( ) => reload ( ) }
151+ disabled = { loading }
152+ className = "gap-2"
153+ title = "Refresh environment status"
154+ >
155+ < RefreshCw className = { `h-3.5 w-3.5 ${ loading ? 'animate-spin' : '' } ` } />
156+ Refresh
157+ </ Button >
110158 < Button
111159 variant = "outline"
112160 className = "gap-2"
113161 onClick = { ( ) => navigate ( {
114162 to : '/environments/$environmentId/packages' ,
115163 params : { environmentId : env . id } ,
116164 } ) }
165+ disabled = { env . status !== 'active' }
117166 >
118167 < Package className = "h-4 w-4" />
119168 Packages
@@ -127,7 +176,56 @@ function EnvironmentOverviewComponent() {
127176 </ div >
128177 </ div >
129178
130- { env . envType === 'production' && (
179+ { env . status === 'provisioning' && (
180+ < Card className = "flex items-start gap-3 border-sky-500/40 bg-sky-500/5 p-4" >
181+ < Loader2 className = "mt-0.5 h-5 w-5 shrink-0 animate-spin text-sky-600" />
182+ < div className = "flex-1 text-sm" >
183+ < p className = "font-medium text-sky-700 dark:text-sky-300" >
184+ Provisioning in progress
185+ </ p >
186+ < p className = "text-muted-foreground" >
187+ We’re allocating the physical database and
188+ minting credentials for this environment. This
189+ normally takes a few seconds — click Refresh to
190+ check the latest status.
191+ </ p >
192+ </ div >
193+ </ Card >
194+ ) }
195+
196+ { env . status === 'failed' && (
197+ < Card className = "flex items-start gap-3 border-red-500/40 bg-red-500/5 p-4" >
198+ < AlertTriangle className = "mt-0.5 h-5 w-5 shrink-0 text-red-600" />
199+ < div className = "flex-1 text-sm" >
200+ < p className = "font-medium text-red-700 dark:text-red-300" >
201+ Provisioning failed
202+ </ p >
203+ < p className = "text-muted-foreground" >
204+ { provisioningError ?. message ??
205+ 'The environment could not be provisioned. Retry to run the driver handshake again.' }
206+ </ p >
207+ { provisioningError ?. failedAt && (
208+ < p className = "mt-1 text-xs text-muted-foreground" >
209+ Last attempt: { new Date ( provisioningError . failedAt ) . toLocaleString ( ) }
210+ </ p >
211+ ) }
212+ < div className = "mt-3" >
213+ < Button
214+ size = "sm"
215+ variant = "destructive"
216+ onClick = { handleRetry }
217+ disabled = { retrying }
218+ className = "gap-2"
219+ >
220+ < RotateCw className = { `h-3.5 w-3.5 ${ retrying ? 'animate-spin' : '' } ` } />
221+ { retrying ? 'Retrying…' : 'Retry provisioning' }
222+ </ Button >
223+ </ div >
224+ </ div >
225+ </ Card >
226+ ) }
227+
228+ { env . envType === 'production' && env . status === 'active' && (
131229 < Card className = "flex items-start gap-3 border-red-500/40 bg-red-500/5 p-4" >
132230 < AlertTriangle className = "mt-0.5 h-5 w-5 shrink-0 text-red-600" />
133231 < div className = "text-sm" >
@@ -193,7 +291,7 @@ function EnvironmentOverviewComponent() {
193291 size = "sm"
194292 variant = "outline"
195293 onClick = { handleRotate }
196- disabled = { rotating }
294+ disabled = { rotating || env . status !== 'active' }
197295 className = "gap-2"
198296 >
199297 < RefreshCw
0 commit comments