feat(widgets): ✨ Add real-time system health metric display to HealthMonitor widget
This commit is contained in:
parent
5f57609578
commit
593d8e2c95
1 changed files with 111 additions and 1 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue