@@ -587,15 +587,120 @@ function renderSecurityCard(props: QuickSettingsProps) {
587587 ` ;
588588}
589589
590- function renderSystemRow ( label : string , value : string , title ?: string ) {
590+ type SystemStat = {
591+ label : string ;
592+ value : string ;
593+ unit ?: string ;
594+ detail ?: string ;
595+ /** Used share of the resource (0..1); renders the meter bar when present. */
596+ usedFraction ?: number ;
597+ title ?: string ;
598+ } ;
599+
600+ // Meter tones reuse the badge palette: calm until 75%, warn to 92%, critical beyond.
601+ function systemMeterTone ( fraction : number ) : "ok" | "warn" | "critical" {
602+ if ( fraction >= 0.92 ) {
603+ return "critical" ;
604+ }
605+ if ( fraction >= 0.75 ) {
606+ return "warn" ;
607+ }
608+ return "ok" ;
609+ }
610+
611+ function renderSystemMeter ( label : string , fraction : number ) {
612+ const clamped = Math . min ( Math . max ( fraction , 0 ) , 1 ) ;
613+ const percent = Math . round ( clamped * 100 ) ;
614+ return html `
615+ < div
616+ class ="qs-meter "
617+ role ="meter "
618+ aria-label ="${ label } usage "
619+ aria-valuemin ="0 "
620+ aria-valuemax ="100 "
621+ aria-valuenow =${ percent }
622+ >
623+ < div
624+ class ="qs-meter__fill qs-meter__fill-- ${ systemMeterTone ( clamped ) } "
625+ style ="--qs-meter-fill: ${ percent } % "
626+ > </ div >
627+ </ div >
628+ ` ;
629+ }
630+
631+ function renderSystemStat ( stat : SystemStat ) {
591632 return html `
592- < div class ="qs-row ">
593- < span class ="qs-row__label "> ${ label } </ span >
594- < span class ="qs-row__value " title =${ title ?? "" } > ${ value } </ span >
633+ < div class ="qs-stat " title =${ stat . title ?? "" } >
634+ < div class ="qs-stat__label "> ${ stat . label } </ div >
635+ < div class ="qs-stat__value ">
636+ ${ stat . value } ${ stat . unit ? html ` < span class ="qs-stat__unit "> ${ stat . unit } </ span > ` : nothing }
637+ </ div >
638+ ${ stat . usedFraction == null ? nothing : renderSystemMeter ( stat . label , stat . usedFraction ) }
639+ ${ stat . detail ? html `< div class ="qs-stat__detail "> ${ stat . detail } </ div > ` : nothing }
595640 </ div >
596641 ` ;
597642}
598643
644+ function usedFraction ( totalBytes : number | undefined , freeBytes : number | undefined ) {
645+ if ( totalBytes == null || freeBytes == null || totalBytes <= 0 ) {
646+ return undefined ;
647+ }
648+ return ( totalBytes - freeBytes ) / totalBytes ;
649+ }
650+
651+ function formatUsedPercent ( fraction : number ) {
652+ return `${ Math . round ( Math . min ( Math . max ( fraction , 0 ) , 1 ) * 100 ) } %` ;
653+ }
654+
655+ function buildSystemStats ( info : SystemInfoResult ) : SystemStat [ ] {
656+ const load = info . loadAverage ?. [ 0 ] ;
657+ const loadTitle = info . loadAverage
658+ ? `Load average: ${ info . loadAverage . map ( ( value ) => value . toFixed ( 1 ) ) . join ( " · " ) } `
659+ : undefined ;
660+ const cpuTitle = [ info . cpuModel , loadTitle ] . filter ( Boolean ) . join ( " · " ) || undefined ;
661+ const coresLabel = `${ info . cpuCount } core${ info . cpuCount === 1 ? "" : "s" } ` ;
662+ const cpu : SystemStat =
663+ load == null
664+ ? { label : "CPU" , value : coresLabel , detail : info . cpuModel , title : cpuTitle }
665+ : {
666+ label : "CPU" ,
667+ value : load . toFixed ( 1 ) ,
668+ unit : "load" ,
669+ detail : coresLabel ,
670+ // 1-minute load over core count approximates saturation; >100% clamps full.
671+ usedFraction : info . cpuCount > 0 ? load / info . cpuCount : undefined ,
672+ title : cpuTitle ,
673+ } ;
674+ const memoryUsed = usedFraction ( info . memoryTotalBytes , info . memoryFreeBytes ) ;
675+ const memory : SystemStat = {
676+ label : "Memory" ,
677+ value : memoryUsed == null ? "—" : formatUsedPercent ( memoryUsed ) ,
678+ unit : memoryUsed == null ? undefined : "used" ,
679+ detail : `${ formatBytes ( info . memoryFreeBytes ) } free of ${ formatBytes ( info . memoryTotalBytes ) } ` ,
680+ usedFraction : memoryUsed ,
681+ } ;
682+ const stats = [ cpu , memory ] ;
683+ const diskUsed = usedFraction ( info . diskTotalBytes , info . diskAvailableBytes ) ;
684+ // Disk info is optional in the protocol; skip the tile instead of showing an empty gauge.
685+ if ( diskUsed != null ) {
686+ stats . push ( {
687+ label : "Disk" ,
688+ value : formatUsedPercent ( diskUsed ) ,
689+ unit : "used" ,
690+ detail : `${ formatBytes ( info . diskAvailableBytes ) } free of ${ formatBytes ( info . diskTotalBytes ) } ` ,
691+ usedFraction : diskUsed ,
692+ title : info . diskPath ,
693+ } ) ;
694+ }
695+ return stats ;
696+ }
697+
698+ const SYSTEM_STATS_PLACEHOLDER : SystemStat [ ] = [
699+ { label : "CPU" , value : "—" } ,
700+ { label : "Memory" , value : "—" } ,
701+ { label : "Disk" , value : "—" } ,
702+ ] ;
703+
599704function renderSystemCard ( props : QuickSettingsProps ) {
600705 if ( props . systemInfoUnavailable ) {
601706 return nothing ;
@@ -605,34 +710,34 @@ function renderSystemCard(props: QuickSettingsProps) {
605710 const hostTitle = info && info . hostname !== info . machineName ? info . hostname : undefined ;
606711 const address = info ?. lanAddress
607712 ? `${ info . lanAddress } ${ info . port == null ? "" : `:${ info . port } ` } `
608- : placeholder ;
609- const osLabel = info ? `${ info . osLabel } · ${ info . arch } ` : placeholder ;
610- const runtime = info ? `Node ${ info . nodeVersion } · PID ${ info . pid } ` : placeholder ;
611- const cpu = info
612- ? `${ info . cpuCount } cores${ info . loadAverage ? ` · load ${ info . loadAverage [ 0 ] . toFixed ( 1 ) } ` : "" } `
613- : placeholder ;
614- const loadTitle = info ?. loadAverage
615- ? `Load average: ${ info . loadAverage . map ( ( value ) => value . toFixed ( 1 ) ) . join ( " · " ) } `
616713 : undefined ;
617- const cpuTitle = [ info ?. cpuModel , loadTitle ] . filter ( Boolean ) . join ( " · " ) || undefined ;
618- const memory = info
619- ? `${ formatBytes ( info . memoryFreeBytes ) } free of ${ formatBytes ( info . memoryTotalBytes ) } `
620- : placeholder ;
621- const hasDisk = info ?. diskAvailableBytes != null && info . diskTotalBytes != null ;
622- const disk = hasDisk
623- ? `${ formatBytes ( info . diskAvailableBytes ) } free of ${ formatBytes ( info . diskTotalBytes ) } `
624- : placeholder ;
714+ const stats = info ? buildSystemStats ( info ) : SYSTEM_STATS_PLACEHOLDER ;
625715
626716 return html `
627717 < div class ="qs-card qs-card--system ">
628- ${ renderCardHeader ( icons . monitor , "Gateway Host" ) }
629- < div class ="qs-card__body ">
630- ${ renderSystemRow ( "Host" , info ?. machineName ?? placeholder , hostTitle ) }
631- ${ renderSystemRow ( "Address" , address ) } ${ renderSystemRow ( "OS" , osLabel ) }
632- ${ renderSystemRow ( "Runtime" , runtime ) }
633- ${ renderSystemRow ( "Uptime" , info ? formatDurationHuman ( info . uptimeMs ) : placeholder ) }
634- ${ renderSystemRow ( "CPU" , cpu , cpuTitle ) } ${ renderSystemRow ( "Memory" , memory ) }
635- ${ info == null || hasDisk ? renderSystemRow ( "Disk" , disk , info ?. diskPath ) : nothing }
718+ ${ renderCardHeader (
719+ icons . monitor ,
720+ "Gateway Host" ,
721+ info
722+ ? html `< span class ="qs-badge qs-badge--ok "
723+ > Up ${ formatDurationHuman ( info . uptimeMs ) } </ span
724+ > `
725+ : undefined ,
726+ ) }
727+ < div class ="qs-card__body qs-system ">
728+ < div class ="qs-system__identity ">
729+ < div class ="qs-system__name " title =${ hostTitle ?? "" } >
730+ ${ info ?. machineName ?? placeholder }
731+ </ div >
732+ < div class ="qs-system__meta ">
733+ ${ info ? `${ info . osLabel } · ${ info . arch } ` : placeholder }
734+ </ div >
735+ < div class ="qs-system__meta ">
736+ ${ info ? `Node ${ info . nodeVersion } · PID ${ info . pid } ` : placeholder }
737+ </ div >
738+ ${ address ? html `< code class ="qs-system__address "> ${ address } </ code > ` : nothing }
739+ </ div >
740+ < div class ="qs-system__stats "> ${ stats . map ( renderSystemStat ) } </ div >
636741 </ div >
637742 </ div >
638743 ` ;
0 commit comments