@@ -28,7 +28,6 @@ import {
2828 RefreshCw ,
2929 CircleCheck ,
3030 CircleX ,
31- Terminal ,
3231} from "lucide-react" ;
3332import { WebTerminal , WebTerminalButton } from "./web-terminal" ;
3433
@@ -128,32 +127,66 @@ export function EnvironmentPanel({
128127 }
129128 } , [ environment , checkServiceStatus ] ) ;
130129
131- // Auto-check service status when environment is running
130+ // Refresh environments list (also fetches individual environment for latest metadata)
131+ const refreshEnvironments = useCallback ( async ( ) => {
132+ try {
133+ // First, get the full list
134+ const listResponse = await fetch ( "/api/environment" ) ;
135+ const listData = await listResponse . json ( ) ;
136+
137+ if ( listData . environments ) {
138+ // For each running environment, fetch its individual data to get latest metadata
139+ // (instancesClient.list may not have the most recent metadata like ttyd-password)
140+ const updatedEnvironments = await Promise . all (
141+ listData . environments . map ( async ( env : DevEnvironment ) => {
142+ if ( env . status === "RUNNING" && ! env . ttydPassword ) {
143+ // Fetch individual environment to get latest metadata
144+ try {
145+ const envResponse = await fetch ( `/api/environment?instanceId=${ encodeURIComponent ( env . instanceId ) } ` ) ;
146+ const envData = await envResponse . json ( ) ;
147+ if ( envData . environment ) {
148+ return envData . environment ;
149+ }
150+ } catch {
151+ // Fall back to list data
152+ }
153+ }
154+ return env ;
155+ } )
156+ ) ;
157+ setEnvironments ( updatedEnvironments ) ;
158+ }
159+ } catch ( err ) {
160+ console . error ( "Error refreshing environments:" , err ) ;
161+ }
162+ } , [ ] ) ;
163+
164+ // Auto-check service status and refresh environment data when running
132165 useEffect ( ( ) => {
133166 if ( environment ?. status === "RUNNING" && environment . externalIp ) {
167+ // Initial checks
134168 checkServiceStatus ( environment . instanceId ) ;
135169
136- // Poll every 30 seconds
170+ // If no password yet, refresh environment data to get it
171+ // (ttyd password is set by startup script which takes a few seconds)
172+ if ( ! environment . ttydPassword ) {
173+ const passwordPollInterval = setInterval ( async ( ) => {
174+ await refreshEnvironments ( ) ;
175+ } , 5000 ) ; // Check every 5 seconds for password
176+
177+ // Stop polling after 60 seconds (startup script should be done by then)
178+ setTimeout ( ( ) => clearInterval ( passwordPollInterval ) , 60000 ) ;
179+ }
180+
181+ // Poll service status every 30 seconds
137182 const interval = setInterval ( ( ) => {
138183 checkServiceStatus ( environment . instanceId ) ;
184+ refreshEnvironments ( ) ; // Also refresh environment data
139185 } , 30000 ) ;
140186
141187 return ( ) => clearInterval ( interval ) ;
142188 }
143- } , [ environment ?. status , environment ?. externalIp , environment ?. instanceId , checkServiceStatus ] ) ;
144-
145- // Refresh environments list
146- const refreshEnvironments = useCallback ( async ( ) => {
147- try {
148- const response = await fetch ( "/api/environment" ) ;
149- const data = await response . json ( ) ;
150- if ( data . environments ) {
151- setEnvironments ( data . environments ) ;
152- }
153- } catch ( err ) {
154- console . error ( "Error refreshing environments:" , err ) ;
155- }
156- } , [ ] ) ;
189+ } , [ environment ?. status , environment ?. externalIp , environment ?. instanceId , environment ?. ttydPassword , checkServiceStatus , refreshEnvironments ] ) ;
157190
158191 // Poll for environment status until target state is reached
159192 const pollUntilStatus = useCallback ( async (
@@ -217,7 +250,30 @@ export function EnvironmentPanel({
217250 const data = await response . json ( ) ;
218251
219252 if ( ! response . ok ) {
220- throw new Error ( data . error || "Failed to create environment" ) ;
253+ const errorMsg = data . error || "Failed to create environment" ;
254+
255+ // Check if this is a quota exhausted error
256+ if ( errorMsg . startsWith ( "QUOTA_EXHAUSTED:" ) ) {
257+ // Parse the error: QUOTA_EXHAUSTED:machineType:message
258+ const parts = errorMsg . split ( ":" ) ;
259+ const exhaustedMachineType = parts [ 1 ] ;
260+ const userMessage = parts . slice ( 2 ) . join ( ":" ) ;
261+
262+ // Find alternative machine types
263+ const currentIndex = MACHINE_CONFIGS . findIndex ( c => c . machineType === exhaustedMachineType ) ;
264+ const alternatives = MACHINE_CONFIGS . filter ( ( _ , i ) => i !== currentIndex ) ;
265+
266+ if ( alternatives . length > 0 ) {
267+ // Suggest the next available machine type
268+ const nextConfig = alternatives [ 0 ] ;
269+ setSelectedMachineType ( nextConfig . machineType ) ;
270+ setShowMachineSelector ( true ) ; // Open the selector so user can see options
271+ }
272+
273+ throw new Error ( userMessage ) ;
274+ }
275+
276+ throw new Error ( errorMsg ) ;
221277 }
222278
223279 // Initial environment created, now poll until RUNNING
@@ -508,7 +564,17 @@ export function EnvironmentPanel({
508564
509565 { error && (
510566 < div className = "mb-6 p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm max-w-md mx-auto" >
511- { error }
567+ < div className = "flex items-start gap-3" >
568+ < AlertCircle className = "w-5 h-5 flex-shrink-0 mt-0.5" />
569+ < div >
570+ < p className = "font-medium" > { error } </ p >
571+ { error . includes ( "unavailable" ) && (
572+ < p className = "mt-2 text-slate-400" >
573+ 👆 Please select a different machine type above and try again.
574+ </ p >
575+ ) }
576+ </ div >
577+ </ div >
512578 </ div >
513579 ) }
514580
@@ -679,51 +745,68 @@ echo "✓ Ready! You can now click 'Open in Cursor/VS Code'"`;
679745 </ p >
680746
681747 { error && (
682- < div className = "mb-6 p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm" >
683- { error }
748+ < div className = "mb-6 p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm max-w-md mx-auto" >
749+ < div className = "flex items-start gap-3" >
750+ < AlertCircle className = "w-5 h-5 flex-shrink-0 mt-0.5" />
751+ < div >
752+ < p className = "font-medium" > { error } </ p >
753+ { error . includes ( "unavailable" ) && (
754+ < p className = "mt-2 text-slate-400" >
755+ The selected machine type is out of capacity. Please try again later or contact support.
756+ </ p >
757+ ) }
758+ </ div >
759+ </ div >
684760 </ div >
685761 ) }
686762
687763 { showNewInstanceForm ? (
688764 newInstanceFormJSX
689765 ) : (
690- < div className = "flex items-center justify-center gap-4" >
691- < button
692- onClick = { ( ) => createEnvironment ( environment . instanceId ) }
693- disabled = { isCreating || isDeleting }
694- className = "inline-flex items-center gap-3 px-8 py-4 rounded-2xl bg-gradient-to-r from-emerald-500 to-cyan-500 text-white font-semibold text-lg hover:from-emerald-400 hover:to-cyan-400 transition-all hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
695- >
696- { isCreating ? (
697- < >
698- < Loader2 className = "w-6 h-6 animate-spin" />
699- Resuming...
700- </ >
701- ) : (
702- < >
703- < Play className = "w-6 h-6" />
704- Resume Environment
705- </ >
706- ) }
707- </ button >
766+ < >
767+ { /* Machine type selector for resume - useful when quota is exhausted */ }
768+ < div className = "max-w-md mx-auto mb-6" >
769+ { machineTypeSelectorJSX }
770+ </ div >
708771
709- < button
710- onClick = { deleteEnvironment }
711- disabled = { isCreating || isDeleting }
712- className = "inline-flex items-center gap-3 px-6 py-4 rounded-2xl bg-red-500/10 text-red-400 font-semibold hover:bg-red-500/20 transition-all disabled:opacity-50"
713- >
714- { isDeleting ? (
715- < >
716- < Loader2 className = "w-5 h-5 animate-spin" />
717- Resetting...
718- </ >
719- ) : (
720- < >
721- < RotateCcw className = "w-5 h-5" />
722- Reset
723- </ >
724- ) }
725- </ button >
726- </ div >
772+ < div className = "flex items-center justify-center gap-4" >
773+ < button
774+ onClick = { ( ) => createEnvironment ( environment . instanceId ) }
775+ disabled = { isCreating || isDeleting }
776+ className = "inline-flex items-center gap-3 px-8 py-4 rounded-2xl bg-gradient-to-r from-emerald-500 to-cyan-500 text-white font-semibold text-lg hover:from-emerald-400 hover:to-cyan-400 transition-all hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
777+ >
778+ { isCreating ? (
779+ < >
780+ < Loader2 className = "w-6 h-6 animate-spin" />
781+ Resuming...
782+ </ >
783+ ) : (
784+ < >
785+ < Play className = "w-6 h-6" />
786+ Resume Environment
787+ </ >
788+ ) }
789+ </ button >
790+
791+ < button
792+ onClick = { deleteEnvironment }
793+ disabled = { isCreating || isDeleting }
794+ className = "inline-flex items-center gap-3 px-6 py-4 rounded-2xl bg-red-500/10 text-red-400 font-semibold hover:bg-red-500/20 transition-all disabled:opacity-50"
795+ >
796+ { isDeleting ? (
797+ < >
798+ < Loader2 className = "w-5 h-5 animate-spin" />
799+ Resetting...
800+ </ >
801+ ) : (
802+ < >
803+ < RotateCcw className = "w-5 h-5" />
804+ Reset
805+ </ >
806+ ) }
807+ </ button >
808+ </ div >
809+ </ >
727810 ) }
728811
729812 < p className = "mt-6 text-sm text-slate-500" >
@@ -885,6 +968,8 @@ echo "✓ SSH configured for all Teable dev environments"`;
885968 ttydPassword = { environment . ttydPassword }
886969 instanceId = { environment . instanceId }
887970 onClose = { ( ) => setShowWebTerminal ( false ) }
971+ onRefresh = { refreshEnvironments }
972+ isRefreshing = { isCheckingServices }
888973 />
889974 ) }
890975
0 commit comments