@@ -103,6 +103,7 @@ const App = () => {
103103 Record < string , { x : number ; y : number } >
104104 > ( { } ) ;
105105 const [ draggedNodeId , setDraggedNodeId ] = useState < string | null > ( null ) ;
106+ const [ buildStartedAt , setBuildStartedAt ] = useState < Date | null > ( null ) ;
106107 const [ nodes , setNodes ] = useNodesState ( [ ] ) ;
107108 const [ edges , setEdges , onEdgesChange ] = useEdgesState ( [ ] ) ;
108109 const flowInstance = useRef < ReactFlowInstance | null > ( null ) ;
@@ -121,6 +122,7 @@ const App = () => {
121122 null
122123 ) ;
123124 const pendingBuildLogRef = useRef ( false ) ;
125+ const lastApiNoticeRef = useRef ( 0 ) ;
124126 const applyTerminalTheme = ( ) => {
125127 if ( ! terminal . current || ! terminalReady . current ) {
126128 return ;
@@ -138,6 +140,19 @@ const App = () => {
138140 effectiveColorScheme === "dark" ? "#3b3f45" : "#c9d0d8" ,
139141 } ;
140142 } ;
143+
144+ const notifyApiUnavailable = ( ) => {
145+ const now = Date . now ( ) ;
146+ if ( now - lastApiNoticeRef . current < 15000 ) {
147+ return ;
148+ }
149+ lastApiNoticeRef . current = now ;
150+ notifications . show ( {
151+ color : "red" ,
152+ title : "API unavailable" ,
153+ message : "Unable to reach the Terrabuild API." ,
154+ } ) ;
155+ } ;
141156 const flushPendingTerminalActions = ( ) => {
142157 if ( ! terminalReady . current ) {
143158 return ;
@@ -278,15 +293,23 @@ const App = () => {
278293
279294 useEffect ( ( ) => {
280295 const load = async ( ) => {
281- const [ targetsRes , projectsRes ] = await Promise . all ( [
282- fetch ( "/api/targets" ) ,
283- fetch ( "/api/projects" ) ,
284- ] ) ;
285- if ( targetsRes . ok ) {
286- setTargets ( await targetsRes . json ( ) ) ;
287- }
288- if ( projectsRes . ok ) {
289- setProjects ( await projectsRes . json ( ) ) ;
296+ try {
297+ const [ targetsRes , projectsRes ] = await Promise . all ( [
298+ fetch ( "/api/targets" ) ,
299+ fetch ( "/api/projects" ) ,
300+ ] ) ;
301+ if ( targetsRes . ok ) {
302+ setTargets ( await targetsRes . json ( ) ) ;
303+ } else {
304+ notifyApiUnavailable ( ) ;
305+ }
306+ if ( projectsRes . ok ) {
307+ setProjects ( await projectsRes . json ( ) ) ;
308+ } else {
309+ notifyApiUnavailable ( ) ;
310+ }
311+ } catch {
312+ notifyApiUnavailable ( ) ;
290313 }
291314 } ;
292315 load ( ) . catch ( ( ) => null ) ;
@@ -345,6 +368,7 @@ const App = () => {
345368 fetchGraph ( ) . catch ( ( ) => {
346369 setGraphError ( "Failed to load graph." ) ;
347370 setGraph ( null ) ;
371+ notifyApiUnavailable ( ) ;
348372 } ) ;
349373 } , [ selectedTargets , selectedProjects , configuration , environment , engine ] ) ;
350374
@@ -510,7 +534,14 @@ const App = () => {
510534 const controller = new AbortController ( ) ;
511535 logAbort . current = controller ;
512536
513- const response = await fetch ( "/api/build/log" , { signal : controller . signal } ) ;
537+ let response : Response ;
538+ try {
539+ response = await fetch ( "/api/build/log" , { signal : controller . signal } ) ;
540+ } catch {
541+ notifyApiUnavailable ( ) ;
542+ setBuildRunning ( false ) ;
543+ return ;
544+ }
514545 if ( ! response . body ) {
515546 return ;
516547 }
@@ -632,6 +663,7 @@ const App = () => {
632663 message : "Build command copied to clipboard." ,
633664 } ) ;
634665 } catch {
666+ notifyApiUnavailable ( ) ;
635667 notifications . show ( {
636668 color : "red" ,
637669 title : "Copy failed" ,
@@ -647,13 +679,22 @@ const App = () => {
647679 setBuildRunning ( true ) ;
648680 setBuildError ( null ) ;
649681 const payload = buildPayload ( ) ;
650- const response = await fetch ( "/api/build" , {
651- method : "POST" ,
652- headers : { "Content-Type" : "application/json" } ,
653- body : JSON . stringify ( payload ) ,
654- } ) ;
682+ let response : Response ;
683+ try {
684+ response = await fetch ( "/api/build" , {
685+ method : "POST" ,
686+ headers : { "Content-Type" : "application/json" } ,
687+ body : JSON . stringify ( payload ) ,
688+ } ) ;
689+ } catch {
690+ setBuildRunning ( false ) ;
691+ setBuildStartedAt ( null ) ;
692+ notifyApiUnavailable ( ) ;
693+ return ;
694+ }
655695 if ( ! response . ok ) {
656696 setBuildRunning ( false ) ;
697+ setBuildStartedAt ( null ) ;
657698 const message = await response . text ( ) ;
658699 setBuildError ( message ) ;
659700 notifications . show ( {
@@ -663,13 +704,15 @@ const App = () => {
663704 } ) ;
664705 return ;
665706 }
707+ setBuildStartedAt ( null ) ;
666708 notifications . show ( {
667709 color : "blue" ,
668710 title : "Build started" ,
669711 message : "Build is running." ,
670712 } ) ;
671713 startLogStream ( ) . catch ( ( ) => {
672714 setBuildRunning ( false ) ;
715+ notifyApiUnavailable ( ) ;
673716 notifications . show ( {
674717 color : "red" ,
675718 title : "Build failed" ,
@@ -708,7 +751,9 @@ const App = () => {
708751 setProjectStatus ( statusMap ) ;
709752 }
710753 } ;
711- refresh ( ) . catch ( ( ) => null ) ;
754+ refresh ( ) . catch ( ( ) => {
755+ notifyApiUnavailable ( ) ;
756+ } ) ;
712757 } , [
713758 buildRunning ,
714759 selectedTargets ,
@@ -749,6 +794,12 @@ const App = () => {
749794 if ( ! terminal . current ) {
750795 return ;
751796 }
797+ const summary = nodeResults [ key ] ;
798+ if ( summary ?. startedAt ) {
799+ setBuildStartedAt ( new Date ( summary . startedAt ) ) ;
800+ } else {
801+ setBuildStartedAt ( null ) ;
802+ }
752803 setSelectedTargetKey ( key ) ;
753804 terminal . current . reset ( ) ;
754805 setShowTerminal ( true ) ;
@@ -779,6 +830,16 @@ const App = () => {
779830 await loadTargetLog ( key , target ) ;
780831 } ;
781832
833+ useEffect ( ( ) => {
834+ if ( ! selectedTargetKey ) {
835+ return ;
836+ }
837+ const summary = nodeResults [ selectedTargetKey ] ;
838+ if ( summary ?. startedAt ) {
839+ setBuildStartedAt ( new Date ( summary . startedAt ) ) ;
840+ }
841+ } , [ selectedTargetKey , nodeResults ] ) ;
842+
782843 const handleNodesChange : OnNodesChange = ( changes ) => {
783844 setNodes ( ( current ) => {
784845 const updated = applyNodeChanges ( changes , current ) ;
@@ -796,6 +857,13 @@ const App = () => {
796857
797858 const terminalBackground =
798859 effectiveColorScheme === "dark" ? theme . colors . dark [ 7 ] : theme . white ;
860+ const buildLogTitle = buildStartedAt
861+ ? `Build Log ${ buildStartedAt
862+ . toISOString ( )
863+ . replace ( "T" , " " )
864+ . replace ( "Z" , "" )
865+ . slice ( 0 , 19 ) } `
866+ : "Build Log" ;
799867
800868 return (
801869 < AppShell
@@ -917,6 +985,7 @@ const App = () => {
917985 < BuildLogPanel
918986 showTerminal = { showTerminal }
919987 buildRunning = { buildRunning }
988+ title = { buildLogTitle }
920989 onHide = { ( ) => setShowTerminal ( false ) }
921990 terminalRef = { terminalRef }
922991 background = { terminalBackground }
0 commit comments