@@ -69,9 +69,15 @@ import { cn } from '@/lib/utils'
6969import {
7070 deleteStaleSystemInstance ,
7171 deleteStaleSystemInstances ,
72+ getNativeSystemResources ,
7273 listSystemInstances ,
7374} from '../api'
74- import type { SystemInstance , SystemInstanceStatus } from '../types'
75+ import type {
76+ SystemInstance ,
77+ SystemInstanceStatus ,
78+ SystemProcessInfo ,
79+ SystemResourceSnapshot ,
80+ } from '../types'
7581
7682const INSTANCE_POLL_INTERVAL_MS = 30_000
7783const INSTANCE_SKELETON_KEYS = [
@@ -394,6 +400,7 @@ function ResourceCell(props: ResourceCellProps) {
394400
395401type SystemInstancesTableProps = {
396402 instances : SystemInstance [ ]
403+ native ?: SystemResourceSnapshot
397404 deletingReporterId : string | null
398405 isDeletingInstance : boolean
399406 onDeleteStaleInstance : ( instance : SystemInstance ) => void
@@ -556,6 +563,188 @@ function StorageCell(props: { instance: SystemInstance }) {
556563 )
557564}
558565
566+ function processInfo (
567+ instance : SystemInstance ,
568+ name : 'core' | 'extensions'
569+ ) : SystemProcessInfo | undefined {
570+ return instance . info ?. resources ?. processes ?. [ name ]
571+ }
572+
573+ function RuntimeMetric ( props : {
574+ label : string
575+ value ?: number
576+ format ?: ( value : number ) => string
577+ } ) {
578+ const { t } = useTranslation ( )
579+ return (
580+ < div className = 'flex items-center justify-between gap-3 text-xs' >
581+ < span className = 'text-muted-foreground' > { props . label } </ span >
582+ < span className = 'font-mono tabular-nums' >
583+ { typeof props . value === 'number'
584+ ? ( props . format ?.( props . value ) ?? String ( props . value ) )
585+ : t ( 'Not sampled' ) }
586+ </ span >
587+ </ div >
588+ )
589+ }
590+
591+ function RuntimeResourceCard ( props : {
592+ label : string
593+ version ?: string
594+ pid ?: number
595+ cpuPercent ?: number
596+ memoryBytes ?: number
597+ memoryBytesLabel ?: string
598+ memoryTotalBytes ?: number
599+ memoryPercent ?: number
600+ } ) {
601+ const { t } = useTranslation ( )
602+ return (
603+ < div className = 'bg-card rounded-md border p-3' >
604+ < div className = 'flex items-center justify-between gap-2' >
605+ < span className = 'text-sm font-medium' > { props . label } </ span >
606+ { props . version ? (
607+ < Badge variant = 'outline' className = 'font-mono text-[10px]' >
608+ { props . version }
609+ </ Badge >
610+ ) : null }
611+ </ div >
612+ < div className = 'mt-2 space-y-1.5' >
613+ < RuntimeMetric
614+ label = { t ( 'CPU' ) }
615+ value = { props . cpuPercent }
616+ format = { formatPercent }
617+ />
618+ < RuntimeMetric
619+ label = { t ( 'Memory' ) }
620+ value = { props . memoryPercent }
621+ format = { formatPercent }
622+ />
623+ { typeof props . memoryBytes === 'number' ? (
624+ < RuntimeMetric
625+ label = { props . memoryBytesLabel ?? t ( 'RSS' ) }
626+ value = { props . memoryBytes }
627+ format = { formatBytes }
628+ />
629+ ) : null }
630+ { typeof props . memoryTotalBytes === 'number' ? (
631+ < RuntimeMetric
632+ label = { t ( 'Total' ) }
633+ value = { props . memoryTotalBytes }
634+ format = { formatBytes }
635+ />
636+ ) : null }
637+ < div className = 'flex items-center justify-between gap-3 text-xs' >
638+ < span className = 'text-muted-foreground' > { t ( 'PID' ) } </ span >
639+ < span className = 'font-mono tabular-nums' >
640+ { typeof props . pid === 'number' ? props . pid : t ( 'Not reported' ) }
641+ </ span >
642+ </ div >
643+ </ div >
644+ </ div >
645+ )
646+ }
647+
648+ type ResourceBreakdownRow = {
649+ instance : SystemInstance | null
650+ system ?: NonNullable <
651+ NonNullable < NonNullable < SystemInstance [ 'info' ] > [ 'resources' ] > [ 'system' ]
652+ >
653+ core ?: SystemProcessInfo
654+ extensions ?: SystemProcessInfo
655+ }
656+
657+ /**
658+ * Shows host-wide samples separately from the two service processes. The
659+ * backend may add these fields independently; absent samples stay explicit.
660+ */
661+ function RuntimeResourceBreakdown ( props : {
662+ instances : SystemInstance [ ]
663+ native ?: SystemResourceSnapshot
664+ } ) {
665+ const { t } = useTranslation ( )
666+ const rows : ResourceBreakdownRow [ ] = props . instances . flatMap ( ( instance ) => {
667+ const resources = instance . info ?. resources
668+ const system = resources ?. system
669+ const core = processInfo ( instance , 'core' )
670+ const extensions = processInfo ( instance , 'extensions' )
671+ if ( ! system && ! core && ! extensions ) return [ ]
672+ return [ { instance, system, core, extensions } ]
673+ } )
674+ if ( props . native ) {
675+ rows . push ( {
676+ instance : null ,
677+ system : {
678+ cpu : { usage_percent : props . native . system . cpu_usage_percent } ,
679+ memory : {
680+ usage_percent : props . native . system . memory_usage_percent ,
681+ used_bytes : props . native . system . memory_used_bytes ,
682+ free_bytes : props . native . system . memory_available_bytes ,
683+ total_bytes : props . native . system . memory_total_bytes ,
684+ } ,
685+ } ,
686+ core : props . native . processes . core ,
687+ extensions : props . native . processes . extensions ,
688+ } )
689+ }
690+ if ( rows . length === 0 ) return null
691+
692+ return (
693+ < section className = 'mt-4 space-y-3 border-t pt-4' >
694+ < div >
695+ < h4 className = 'text-sm font-semibold' > { t ( 'Service resource breakdown' ) } </ h4 >
696+ < p className = 'text-muted-foreground mt-0.5 text-xs' >
697+ { t (
698+ 'System samples describe the host. Core and Extensions values describe their own processes.'
699+ ) }
700+ </ p >
701+ </ div >
702+ < div className = 'grid gap-3 lg:grid-cols-3' >
703+ { rows . map ( ( { instance, system, core, extensions } ) => {
704+ const key = instance ? getReporterId ( instance ) : 'native-services'
705+ return (
706+ < div key = { key } className = 'space-y-2' >
707+ < div className = 'text-muted-foreground truncate font-mono text-[11px]' >
708+ { instance ? getInstanceDisplayName ( instance ) : t ( 'Native services' ) }
709+ </ div >
710+ { system ? (
711+ < RuntimeResourceCard
712+ label = { t ( 'System' ) }
713+ cpuPercent = { system . cpu ?. usage_percent }
714+ memoryBytes = { system . memory ?. used_bytes }
715+ memoryBytesLabel = { t ( 'Used' ) }
716+ memoryTotalBytes = { system . memory ?. total_bytes }
717+ memoryPercent = { system . memory ?. usage_percent }
718+ />
719+ ) : null }
720+ { core ? (
721+ < RuntimeResourceCard
722+ label = { t ( 'Core process' ) }
723+ version = { core . version }
724+ pid = { core . pid }
725+ cpuPercent = { core . cpu_percent }
726+ memoryBytes = { core . memory_bytes }
727+ memoryPercent = { core . memory_percent }
728+ />
729+ ) : null }
730+ { extensions ? (
731+ < RuntimeResourceCard
732+ label = { t ( 'Extensions process' ) }
733+ version = { extensions . version }
734+ pid = { extensions . pid }
735+ cpuPercent = { extensions . cpu_percent }
736+ memoryBytes = { extensions . memory_bytes }
737+ memoryPercent = { extensions . memory_percent }
738+ />
739+ ) : null }
740+ </ div >
741+ )
742+ } ) }
743+ </ div >
744+ </ section >
745+ )
746+ }
747+
559748/**
560749 * Destructive row action. Kept as a real text button at `h-9` so the touch
561750 * target stays usable on phones, where the icon-only variant was too small.
@@ -802,6 +991,10 @@ function SystemInstancesList(props: SystemInstancesTableProps) {
802991 </ TableBody >
803992 </ Table >
804993 </ div >
994+ < RuntimeResourceBreakdown
995+ instances = { props . instances }
996+ native = { props . native }
997+ />
805998 </ >
806999 )
8071000}
@@ -827,6 +1020,13 @@ export function SystemInstancesPanel() {
8271020 retry : false ,
8281021 refetchInterval : INSTANCE_POLL_INTERVAL_MS ,
8291022 } )
1023+ const nativeResourcesQuery = useQuery ( {
1024+ queryKey : [ 'system-info' , 'native-resources' ] ,
1025+ queryFn : getNativeSystemResources ,
1026+ staleTime : 30 * 1000 ,
1027+ retry : false ,
1028+ refetchInterval : INSTANCE_POLL_INTERVAL_MS ,
1029+ } )
8301030
8311031 const instances = instancesQuery . data ?? [ ]
8321032 const staleInstances = instances . filter (
@@ -948,6 +1148,7 @@ export function SystemInstancesPanel() {
9481148 < div className = 'p-4 sm:p-5' >
9491149 < SystemInstancesList
9501150 instances = { instances }
1151+ native = { nativeResourcesQuery . data }
9511152 deletingReporterId = { deletingReporterId }
9521153 isDeletingInstance = {
9531154 isMutatingInstance || deleteStaleInstancesMutation . isPending
0 commit comments