Compare commits

...

1 Commits

Author SHA1 Message Date
Rex
f29eaa9685 CUB-57: Dashboard summary component with status breakdown
All checks were successful
Dev Build / build-test (pull_request) Successful in 3m13s
2026-04-29 11:27:30 -04:00
5 changed files with 344 additions and 1 deletions

View File

@@ -0,0 +1,32 @@
<!-- ============================================================================
Dashboard Summary Component Template
Displays total active agents, status breakdown, and system health.
============================================================================ -->
<section class="dashboard-summary" role="region" aria-label="Dashboard summary">
<!-- Total Agents -->
<div class="summary-total">
<span class="summary-total__count">{{ totalAgents() }}</span>
<span class="summary-total__label">Total Agents</span>
</div>
<!-- Status Breakdown -->
<div class="summary-breakdown" role="list" aria-label="Agent status breakdown">
@for (item of statusItems(); track item.status) {
<div class="status-item {{ statusClass(item.status) }}" role="listitem">
<span class="status-item__dot" [attr.aria-hidden]="true"></span>
<span class="status-item__label">{{ item.label }}</span>
<span class="status-item__count">{{ item.count }}</span>
</div>
}
</div>
<!-- System Health Indicator -->
<div class="health-indicator {{ healthClass() }}" role="status" aria-label="System health: {{ healthLabel() }}">
<span class="health-indicator__dot" [attr.aria-hidden]="true"></span>
<span class="health-indicator__label">System</span>
<span class="health-indicator__value">{{ healthLabel() }}</span>
</div>
</section>

View File

@@ -0,0 +1,218 @@
// ============================================================================
// Dashboard Summary Component Styles
// Uses design tokens from styles/_tokens.scss (CUB-21)
// ============================================================================
@use '../../../styles/tokens' as tokens;
// ---------------------------------------------------------------------------
// Host block
// ---------------------------------------------------------------------------
:host {
display: block;
}
// ---------------------------------------------------------------------------
// Summary container
// ---------------------------------------------------------------------------
.dashboard-summary {
display: flex;
flex-direction: column;
gap: tokens.$spacing-5;
padding: tokens.$spacing-6;
background-color: tokens.$color-surface-medium;
border-radius: tokens.$card-border-radius;
border: 1px solid tokens.$color-surface-lighter;
box-shadow: tokens.$shadow-level-1;
}
// ---------------------------------------------------------------------------
// Total agents
// ---------------------------------------------------------------------------
.summary-total {
display: flex;
flex-direction: column;
align-items: center;
gap: tokens.$spacing-1;
padding-bottom: tokens.$spacing-4;
border-bottom: 1px solid tokens.$color-surface-lighter;
&__count {
font-family: tokens.$font-family-brand;
font-size: tokens.$font-size-display-small;
font-weight: tokens.$font-weight-heavy;
line-height: tokens.$line-height-tight;
color: tokens.$color-on-surface;
letter-spacing: tokens.$letter-spacing-tight;
}
&__label {
font-family: tokens.$font-family-body;
font-size: tokens.$font-size-label-large;
font-weight: tokens.$font-weight-medium;
color: tokens.$color-on-surface-variant;
text-transform: uppercase;
letter-spacing: tokens.$letter-spacing-wide;
}
}
// ---------------------------------------------------------------------------
// Status breakdown
// ---------------------------------------------------------------------------
.summary-breakdown {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: tokens.$spacing-3;
}
// ---------------------------------------------------------------------------
// Individual status item
// ---------------------------------------------------------------------------
.status-item {
display: flex;
align-items: center;
gap: tokens.$spacing-2;
padding: tokens.$spacing-3 tokens.$spacing-4;
border-radius: tokens.$radius-md;
background-color: tokens.$color-surface-dark;
border: 1px solid tokens.$color-surface-lighter;
transition: background-color tokens.$duration-short tokens.$easing-standard;
&__dot {
width: tokens.$status-dot-size;
height: tokens.$status-dot-size;
border-radius: tokens.$radius-full;
flex-shrink: 0;
}
&__label {
flex: 1;
font-family: tokens.$font-family-body;
font-size: tokens.$font-size-body-medium;
font-weight: tokens.$font-weight-medium;
color: tokens.$color-on-surface-variant;
}
&__count {
font-family: tokens.$font-family-mono;
font-size: tokens.$font-size-body-large;
font-weight: tokens.$font-weight-bold;
color: tokens.$color-on-surface;
}
// Status-specific colors using the token map
&--active {
.status-item__dot {
background-color: tokens.$status-active;
animation: pulse-active tokens.$duration-standard tokens.$easing-standard infinite;
}
.status-item__count { color: tokens.$status-active; }
}
&--idle {
.status-item__dot {
background-color: tokens.$status-idle;
}
.status-item__count { color: tokens.$status-idle; }
}
&--thinking {
.status-item__dot {
background-color: tokens.$status-thinking;
animation: pulse-thinking 3s tokens.$easing-standard infinite;
}
.status-item__count { color: tokens.$status-thinking; }
}
&--error {
.status-item__dot {
background-color: tokens.$status-error;
animation: pulse-error tokens.$duration-fast tokens.$easing-standard infinite;
}
.status-item__count { color: tokens.$status-error; }
}
}
// ---------------------------------------------------------------------------
// System health indicator
// ---------------------------------------------------------------------------
.health-indicator {
display: flex;
align-items: center;
gap: tokens.$spacing-2;
padding: tokens.$spacing-3 tokens.$spacing-4;
border-radius: tokens.$radius-md;
border: 1px solid tokens.$color-surface-lighter;
&__dot {
width: tokens.$status-dot-size;
height: tokens.$status-dot-size;
border-radius: tokens.$radius-full;
flex-shrink: 0;
}
&__label {
font-family: tokens.$font-family-body;
font-size: tokens.$font-size-body-medium;
font-weight: tokens.$font-weight-medium;
color: tokens.$color-on-surface-variant;
}
&__value {
margin-left: auto;
font-family: tokens.$font-family-mono;
font-size: tokens.$font-size-body-medium;
font-weight: tokens.$font-weight-bold;
letter-spacing: tokens.$letter-spacing-mono;
}
&--healthy {
.health-indicator__dot {
background-color: tokens.$status-active;
animation: pulse-active tokens.$duration-standard tokens.$easing-standard infinite;
}
.health-indicator__value { color: tokens.$status-active; }
}
&--degraded {
.health-indicator__dot {
background-color: tokens.$status-error;
animation: pulse-error tokens.$duration-fast tokens.$easing-standard infinite;
}
.health-indicator__value { color: tokens.$status-error; }
}
&--unknown {
.health-indicator__dot {
background-color: tokens.$status-offline;
}
.health-indicator__value { color: tokens.$status-offline; }
}
}
// ---------------------------------------------------------------------------
// Pulse animations (from the design system)
// ---------------------------------------------------------------------------
@keyframes pulse-active {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.85); }
}
@keyframes pulse-thinking {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.9); }
}
@keyframes pulse-error {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(0.8); }
}
// ---------------------------------------------------------------------------
// Responsive: stack on compact screens
// ---------------------------------------------------------------------------
@media (max-width: tokens.$breakpoint-compact) {
.summary-breakdown {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,91 @@
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { AgentStatusService } from '../../services/agent-status.service';
import { AgentStatus } from '../../models/agent.model';
/**
* Dashboard Summary Component.
* Displays total active agents, status breakdown (Active, Idle, Thinking, Error),
* and a system health indicator.
*
* Binds to AgentStatusService.agents signal — no hardcoded values.
* Uses design tokens from CUB-21 (styles/_tokens.scss, app/design/tokens.ts).
*/
@Component({
selector: 'app-dashboard-summary',
standalone: true,
imports: [],
templateUrl: './dashboard-summary.component.html',
styleUrl: './dashboard-summary.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardSummaryComponent {
private readonly agentStatusService = inject(AgentStatusService);
/** All agents from the reactive signal */
readonly agents = this.agentStatusService.agents;
/** Total number of agents */
readonly totalAgents = computed(() => this.agents().length);
/** Count of agents per status */
readonly statusCounts = computed(() => {
const agents = this.agents();
const counts: Record<AgentStatus, number> = {
active: 0,
idle: 0,
thinking: 0,
error: 0,
offline: 0,
};
for (const agent of agents) {
counts[agent.status]++;
}
return counts;
});
/** Status items for template iteration */
readonly statusItems = computed(() => {
const counts = this.statusCounts();
const total = this.totalAgents();
return [
{ status: 'active' as AgentStatus, count: counts.active, label: 'Active' },
{ status: 'idle' as AgentStatus, count: counts.idle, label: 'Idle' },
{ status: 'thinking' as AgentStatus, count: counts.thinking, label: 'Thinking' },
{ status: 'error' as AgentStatus, count: counts.error, label: 'Error' },
] as const;
});
/**
* System health indicator:
* - 'healthy' → no errors, at least one active agent
* - 'degraded' → has errors OR no active agents but some agents exist
* - 'unknown' → no agents registered
*/
readonly systemHealth = computed((): 'healthy' | 'degraded' | 'unknown' => {
const agents = this.agents();
if (agents.length === 0) return 'unknown';
const counts = this.statusCounts();
if (counts.error > 0) return 'degraded';
if (counts.active > 0) return 'healthy';
return 'degraded';
});
readonly healthLabel = computed(() => {
const health = this.systemHealth();
switch (health) {
case 'healthy': return 'Healthy';
case 'degraded': return 'Degraded';
case 'unknown': return 'Unknown';
}
});
/** Get the CSS class for a given status */
statusClass(status: AgentStatus): string {
return `status-item--${status}`;
}
/** Get the CSS class for system health */
healthClass(): string {
return `health-indicator--${this.systemHealth()}`;
}
}

View File

@@ -0,0 +1 @@
export { DashboardSummaryComponent } from './dashboard-summary.component';

View File

@@ -3,3 +3,4 @@ export { AgentStatusBadgeComponent } from './agent-status-badge/agent-status-bad
export { QuickJumpDrawerComponent } from './quick-jump-drawer/index';
export { AgentSessionDrawerComponent } from './agent-session-drawer/index';
export type { SessionLogLine, SessionMessage } from './agent-session-drawer/index';
export { DashboardSummaryComponent } from './dashboard-summary/index';