Compare commits
1 Commits
e8ced74429
...
agent/rex/
| Author | SHA1 | Date | |
|---|---|---|---|
| f29eaa9685 |
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()}`;
|
||||
}
|
||||
}
|
||||
1
frontend/src/app/components/dashboard-summary/index.ts
Normal file
1
frontend/src/app/components/dashboard-summary/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DashboardSummaryComponent } from './dashboard-summary.component';
|
||||
@@ -2,4 +2,5 @@ export * from './quick-jump-button/quick-jump-button.component';
|
||||
export { AgentStatusBadgeComponent } from './agent-status-badge/agent-status-badge.component';
|
||||
export { QuickJumpDrawerComponent } from './quick-jump-drawer/index';
|
||||
export { AgentSessionDrawerComponent } from './agent-session-drawer/index';
|
||||
export type { SessionLogLine, SessionMessage } from './agent-session-drawer/index';
|
||||
export type { SessionLogLine, SessionMessage } from './agent-session-drawer/index';
|
||||
export { DashboardSummaryComponent } from './dashboard-summary/index';
|
||||
Reference in New Issue
Block a user