feat(widgets): Add real-time system health metric display to HealthMonitor widget

This commit is contained in:
Lilith 2026-01-20 03:29:37 -08:00
parent 5f57609578
commit 593d8e2c95

View file

@ -29,6 +29,8 @@ export interface HealthMonitorOptions {
maxVisible?: number
/** Use horizontal layout (items side by side) */
horizontal?: boolean
/** Group items by feature prefix (e.g., analytics.api → analytics group) */
grouped?: boolean
/** Box options */
boxOptions?: blessed.Widgets.BoxOptions
}
@ -43,10 +45,12 @@ export class HealthMonitor {
private pendingChecks = new Set<string>()
private maxVisible: number
private horizontal: boolean
private grouped: boolean
constructor(parent: blessed.Widgets.Screen | blessed.Widgets.Node, options: HealthMonitorOptions = {}) {
this.maxVisible = options.maxVisible ?? 20
this.horizontal = options.horizontal ?? false
this.grouped = options.grouped ?? false
this.box = blessed.box({
parent,
@ -136,7 +140,9 @@ export class HealthMonitor {
* Render the widget (call from animation loop for spinner updates)
*/
render(): void {
if (this.horizontal) {
if (this.grouped) {
this.renderGrouped()
} else if (this.horizontal) {
this.renderHorizontal()
} else {
this.renderVertical()
@ -215,6 +221,110 @@ export class HealthMonitor {
this.box.screen.render()
}
private renderGrouped(): void {
const lines: string[] = []
const spinner = this.getSpinnerChar()
// Group health checks by feature
const groups = new Map<string, HealthStatus[]>()
const checks = Array.from(this.healthChecks.values())
for (const check of checks) {
const feature = this.extractFeature(check.service)
const group = groups.get(feature) || []
group.push(check)
groups.set(feature, group)
}
// Sort groups by name
const sortedGroups = Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0]))
// Render each group
for (const [feature, groupChecks] of sortedGroups) {
// Group header with counts
const healthy = groupChecks.filter(c => c.status === 'healthy').length
const checking = groupChecks.filter(c => c.status === 'checking').length
const unhealthy = groupChecks.filter(c => c.status === 'unhealthy').length
let statusSummary = ''
if (healthy > 0) statusSummary += `{green-fg}${healthy}✓{/}`
if (checking > 0) statusSummary += `${statusSummary ? ' ' : ''}{yellow-fg}${checking}◐{/}`
if (unhealthy > 0) statusSummary += `${statusSummary ? ' ' : ''}{red-fg}${unhealthy}✗{/}`
lines.push(`{bold}{cyan-fg}${feature}{/}{/} {gray-fg}(${statusSummary}){/}`)
// Sort checks within group by service name
groupChecks.sort((a, b) => a.service.localeCompare(b.service))
for (const check of groupChecks) {
const symbol = check.status === 'healthy' ? '✓' : check.status === 'unhealthy' ? '✗' : spinner
const color = check.status === 'healthy' ? 'green' : check.status === 'unhealthy' ? 'red' : 'yellow'
const time = check.responseTime !== undefined ? ` {gray-fg}(${check.responseTime}ms){/}` : ''
const shortName = this.extractShortName(check.service)
lines.push(` {${color}-fg}${symbol}{/} ${shortName}:${check.port}${time}`)
}
}
// Show pending checks that aren't in healthChecks yet
const pendingNotInChecks = Array.from(this.pendingChecks).filter(
service => !checks.some(c => c.service === service)
)
if (pendingNotInChecks.length > 0) {
if (lines.length > 0) lines.push('')
lines.push(`{bold}{yellow-fg}Waiting{/}{/}`)
for (const service of pendingNotInChecks.sort().slice(0, 5)) {
lines.push(` {yellow-fg}${spinner}{/} ${service}`)
}
if (pendingNotInChecks.length > 5) {
lines.push(` {gray-fg}+${pendingNotInChecks.length - 5} more...{/}`)
}
}
this.box.setContent(lines.join('\n'))
this.box.screen.render()
}
/**
* Extract feature name from service identifier
* Examples:
* - "lilith-dev-postgres" "docker"
* - "lilith-dev-nginx" "docker"
* - "analytics.api" "analytics"
* - "landing.frontend" "landing"
* - "sso.api" "sso"
*/
private extractFeature(service: string): string {
// Docker containers have "lilith-dev-" prefix
if (service.startsWith('lilith-dev-')) {
return 'docker'
}
// Service format: "feature.component"
const dotIndex = service.indexOf('.')
if (dotIndex > 0) {
return service.substring(0, dotIndex)
}
// Fallback to full name
return service
}
/**
* Extract short name from service identifier
* Examples:
* - "lilith-dev-postgres" "postgres"
* - "analytics.api" "api"
*/
private extractShortName(service: string): string {
if (service.startsWith('lilith-dev-')) {
return service.substring('lilith-dev-'.length)
}
const dotIndex = service.indexOf('.')
if (dotIndex > 0) {
return service.substring(dotIndex + 1)
}
return service
}
/**
* Clear all health checks
*/