@@ -9,6 +9,7 @@ import advanced from "dayjs/plugin/advancedFormat";
99import timezone from "dayjs/plugin/timezone" ;
1010import { Box , render , Text } from "ink" ;
1111import type { SFCNodes } from "@sfcompute/nodes-sdk-alpha" ;
12+ import { formatDuration , intervalToDuration } from "date-fns" ;
1213
1314import { getAuthToken } from "../../helpers/config.ts" ;
1415import { logAndQuit } from "../../helpers/errors.ts" ;
@@ -27,6 +28,7 @@ import {
2728 jsonOption ,
2829 pluralizeNodes ,
2930 printNodeType ,
31+ printVMStatus ,
3032} from "./utils.ts" ;
3133
3234dayjs . extend ( utc ) ;
@@ -63,7 +65,7 @@ function VMTable({ vms }: { vms: NonNullable<SFCNodes.Node["vms"]>["data"] }) {
6365 borderColor = "gray"
6466 >
6567 < Box width = { 25 } padding = { 0 } >
66- < Text bold color = "cyan" > Virtual Machines </ Text >
68+ < Text bold color = "cyan" > Previous VMs </ Text >
6769 </ Box >
6870 < Box width = { 15 } padding = { 0 } >
6971 < Text bold color = "cyan" > Status</ Text >
@@ -268,12 +270,24 @@ function getActionsForNode(node: SFCNodes.Node) {
268270// Component for displaying a single node in verbose format
269271function NodeVerboseDisplay ( { node } : { node : SFCNodes . Node } ) {
270272 // Convert Unix timestamps to dates and calculate duration
271- const startDate = node . start_at && dayjs . unix ( node . start_at ) ;
272- const endDate = node . end_at && dayjs . unix ( node . end_at ) ;
273+ const startDate = node . start_at ? dayjs . unix ( node . start_at ) : null ;
274+ const endDate = node . end_at ? dayjs . unix ( node . end_at ) : null ;
273275 let duration = endDate && startDate && endDate . diff ( startDate , "hours" ) ;
274276 if ( typeof duration === "number" && duration < 1 ) {
275277 duration = 1 ;
276278 }
279+ const durationLabel = duration
280+ ? formatDuration (
281+ intervalToDuration ( {
282+ start : 0 ,
283+ end : duration * 60 * 60 * 1000 ,
284+ } ) ,
285+ {
286+ delimiter : ", " ,
287+ format : [ "years" , "months" , "weeks" , "days" , "hours" ] ,
288+ } ,
289+ )
290+ : null ;
277291 // Convert max_price_per_node_hour from cents to dollars
278292 const pricePerHour = node . max_price_per_node_hour
279293 ? ( node . max_price_per_node_hour / 100 )
@@ -285,6 +299,8 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) {
285299 // Get available actions for this node
286300 const nodeActions = getActionsForNode ( node ) ;
287301
302+ const lastVM = getLastVM ( node ) ;
303+
288304 return (
289305 < Box
290306 borderStyle = "single"
@@ -320,8 +336,41 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) {
320336 < Row head = "Owner: " value = { node . owner } />
321337 </ Box >
322338
339+ { lastVM && (
340+ < >
341+ < Box marginTop = { 1 } paddingX = { 1 } >
342+ < Text bold color = "cyan" > Active VM:</ Text >
343+ </ Box >
344+ < Box marginLeft = { 3 } flexDirection = "column" paddingX = { 1 } >
345+ < Row head = "ID: " value = { lastVM . id } />
346+ < Row head = "Status: " value = { getVMStatusColor ( lastVM . status ) } />
347+ < Row
348+ head = "Start: "
349+ value = { lastVM . start_at
350+ ? dayjs . unix ( lastVM . start_at ) . format ( "YYYY-MM-DDTHH:mm:ssZ" )
351+ : "Not specified" }
352+ />
353+ < Row
354+ head = "End: "
355+ value = { lastVM . end_at
356+ ? dayjs . unix ( lastVM . end_at ) . format ( "YYYY-MM-DDTHH:mm:ssZ" )
357+ : "Not specified" }
358+ />
359+ < Row head = "Image: " value = { lastVM . image_id ?? "Not specified" } />
360+ </ Box >
361+ </ >
362+ ) }
363+
364+ { node . vms ?. data && node . vms . data . length > 1 && (
365+ < Box flexDirection = "column" gap = { 0 } marginLeft = { 1 } marginRight = { 2 } >
366+ < Box marginTop = { 1 } paddingX = { 1 } >
367+ </ Box >
368+ < VMTable vms = { node . vms . data . slice ( 1 ) } />
369+ </ Box >
370+ ) }
371+
323372 < Box marginTop = { 1 } paddingX = { 1 } >
324- < Text > 📅 Schedule:</ Text >
373+ < Text bold color = "cyan" > Schedule:</ Text >
325374 </ Box >
326375
327376 < Box marginLeft = { 3 } flexDirection = "column" paddingX = { 1 } >
@@ -354,15 +403,15 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) {
354403 { duration && (
355404 < Row
356405 head = "Duration: "
357- value = { ` ${ duration } hours` }
406+ value = { durationLabel }
358407 />
359408 ) }
360409 </ Box >
361410
362411 { node . max_price_per_node_hour && (
363412 < >
364413 < Box marginTop = { 1 } paddingX = { 1 } >
365- < Text > 💰 Pricing:</ Text >
414+ < Text bold color = "cyan" > Pricing:</ Text >
366415 </ Box >
367416 < Box marginLeft = { 3 } flexDirection = "column" paddingX = { 1 } >
368417 { node . node_type === "autoreserved" && (
@@ -392,22 +441,10 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) {
392441 </ >
393442 ) }
394443
395- { /* VMs Section - Show if node has VMs */ }
396- { node . vms ?. data && node . vms . data . length > 0 && (
397- < Box flexDirection = "row" gap = { 0 } >
398- < Box marginTop = { 1 } paddingX = { 1 } >
399- < Text color = "cyan" bold > 💿</ Text >
400- </ Box >
401- < Box marginTop = { 1 } >
402- < VMTable vms = { node . vms . data } />
403- </ Box >
404- </ Box >
405- ) }
406-
407444 { node . vms ?. data ?. [ 0 ] ?. image_id && (
408445 < >
409446 < Box marginTop = { 1 } paddingX = { 1 } >
410- < Text > 💾 Current VM Image:</ Text >
447+ < Text bold color = "cyan" > Current VM Image:</ Text >
411448 </ Box >
412449 < Box marginLeft = { 3 } flexDirection = "column" paddingX = { 1 } >
413450 < Row
@@ -422,7 +459,7 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) {
422459 { nodeActions . length > 0 && (
423460 < >
424461 < Box marginTop = { 1 } paddingX = { 1 } >
425- < Text > 🎯 Actions:</ Text >
462+ < Text bold color = "cyan" > Actions:</ Text >
426463 </ Box >
427464 < Box marginLeft = { 3 } flexDirection = "column" paddingX = { 1 } >
428465 { nodeActions . map ( ( action , index ) => (
0 commit comments