diff --git a/ui/src/pages/AgentHealth/AgentHealth.module.css b/ui/src/pages/AgentHealth/AgentHealth.module.css new file mode 100644 index 00000000..814e1c2c --- /dev/null +++ b/ui/src/pages/AgentHealth/AgentHealth.module.css @@ -0,0 +1,66 @@ +.statStrip { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + margin-bottom: 16px; +} + +.groupGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-bottom: 20px; +} + +.instanceRow { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + cursor: pointer; + transition: background 0.1s; + border-bottom: 1px solid var(--border-subtle); + font-size: 12px; +} + +.instanceRow:last-child { + border-bottom: none; +} + +.instanceRow:hover { + background: var(--bg-hover); +} + +.instanceName { + font-weight: 600; + color: var(--text-primary); +} + +.instanceTps { + margin-left: auto; + font-size: 11px; + font-family: var(--font-mono); + color: var(--text-muted); +} + +.eventCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; + display: flex; + flex-direction: column; + max-height: 420px; +} + +.eventCardHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--border-subtle); + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} diff --git a/ui/src/pages/AgentHealth/AgentHealth.tsx b/ui/src/pages/AgentHealth/AgentHealth.tsx index 53f3ccd4..f3a30f66 100644 --- a/ui/src/pages/AgentHealth/AgentHealth.tsx +++ b/ui/src/pages/AgentHealth/AgentHealth.tsx @@ -4,6 +4,7 @@ import { StatCard, StatusDot, Badge, MonoText, GroupCard, EventFeed, } from '@cameleer/design-system'; +import styles from './AgentHealth.module.css'; import { useAgents, useAgentEvents } from '../../api/queries/agents'; import { useRouteCatalog } from '../../api/queries/catalog'; @@ -46,14 +47,14 @@ export default function AgentHealth() { return (
-
+
-
+
{Object.entries(apps).map(([group, groupAgents]) => ( (
{ e.stopPropagation(); navigate(`/agents/${group}/${agent.id}`); }} > - {agent.name} + {agent.name} - {agent.tps > 0 && {agent.tps.toFixed(1)} tps} + {agent.tps > 0 && {agent.tps.toFixed(1)} tps}
))}
@@ -83,8 +84,8 @@ export default function AgentHealth() {
{feedEvents.length > 0 && ( -
-

Event Log

+
+
Event Log
)} diff --git a/ui/src/pages/AgentInstance/AgentInstance.module.css b/ui/src/pages/AgentInstance/AgentInstance.module.css new file mode 100644 index 00000000..3008d533 --- /dev/null +++ b/ui/src/pages/AgentInstance/AgentInstance.module.css @@ -0,0 +1,92 @@ +.statStrip { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + margin-bottom: 16px; +} + +.agentHeader { + display: flex; + align-items: center; + gap: 12px; + margin: 16px 0; +} + +.agentHeader h2 { + font-size: 18px; + font-weight: 600; +} + +.routeBadges { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.chartsGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; + margin-bottom: 20px; +} + +.chartCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: 16px; + overflow: hidden; +} + +.chartHeader { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.chartTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.sectionTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 12px; +} + +.eventCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; + display: flex; + flex-direction: column; + max-height: 420px; + margin-bottom: 20px; +} + +.eventCardHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 16px; + border-bottom: 1px solid var(--border-subtle); + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.infoCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: 16px; +} diff --git a/ui/src/pages/AgentInstance/AgentInstance.tsx b/ui/src/pages/AgentInstance/AgentInstance.tsx index c9828822..4511d2a5 100644 --- a/ui/src/pages/AgentInstance/AgentInstance.tsx +++ b/ui/src/pages/AgentInstance/AgentInstance.tsx @@ -1,10 +1,11 @@ import { useMemo } from 'react'; import { useParams } from 'react-router'; import { - StatCard, StatusDot, Badge, MonoText, Card, + StatCard, StatusDot, Badge, LineChart, AreaChart, EventFeed, Breadcrumb, Spinner, - SectionHeader, CodeBlock, + CodeBlock, } from '@cameleer/design-system'; +import styles from './AgentInstance.module.css'; import { useAgents, useAgentEvents } from '../../api/queries/agents'; import { useStatsTimeseries } from '../../api/queries/executions'; import { useGlobalFilters } from '@cameleer/design-system'; @@ -59,21 +60,21 @@ export default function AgentInstance() { {agent && ( <> -
+

{agent.name}

-
+
0.05 ? 'error' : undefined} />
- Routes -
+
Routes
+
{(agent.routeIds || []).map((r: string) => ( ))} @@ -83,36 +84,40 @@ export default function AgentInstance() { {chartData.length > 0 && ( <> - Performance -
- ({ x: i, y: d.throughput })) }]} height={200} /> - ({ x: i, y: d.latency })) }]} height={200} /> +
Performance
+
+
+
Throughput
+ ({ x: i, y: d.throughput })) }]} height={200} /> +
+
+
Latency
+ ({ x: i, y: d.latency })) }]} height={200} /> +
)} {feedEvents.length > 0 && ( - <> - Events +
+
Events
- +
)} {agent && ( <> - Agent Info - -
- -
-
+
Agent Info
+
+ +
)}
diff --git a/ui/src/pages/Dashboard/Dashboard.module.css b/ui/src/pages/Dashboard/Dashboard.module.css new file mode 100644 index 00000000..a0867aa2 --- /dev/null +++ b/ui/src/pages/Dashboard/Dashboard.module.css @@ -0,0 +1,84 @@ +.healthStrip { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 10px; + margin-bottom: 16px; +} + +.tableSection { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; +} + +.tableHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.tableTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.tableRight { + display: flex; + align-items: center; + gap: 10px; +} + +.tableMeta { + font-size: 11px; + color: var(--text-muted); + font-family: var(--font-mono); +} + +.panelSection { + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.panelSection:last-child { + border-bottom: none; + margin-bottom: 0; + padding-bottom: 0; +} + +.panelSectionTitle { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-muted); + margin-bottom: 10px; +} + +.overviewGrid { + display: flex; + flex-direction: column; + gap: 8px; +} + +.overviewRow { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.overviewLabel { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--text-muted); + width: 90px; + flex-shrink: 0; + padding-top: 2px; +} diff --git a/ui/src/pages/Dashboard/Dashboard.tsx b/ui/src/pages/Dashboard/Dashboard.tsx index 81942fd7..4947702e 100644 --- a/ui/src/pages/Dashboard/Dashboard.tsx +++ b/ui/src/pages/Dashboard/Dashboard.tsx @@ -8,6 +8,7 @@ import type { Column } from '@cameleer/design-system'; import { useSearchExecutions, useExecutionStats, useStatsTimeseries, useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions'; import { useGlobalFilters } from '@cameleer/design-system'; import type { ExecutionSummary } from '../../api/types'; +import styles from './Dashboard.module.css'; interface Row extends ExecutionSummary { id: string } @@ -61,12 +62,32 @@ export default function Dashboard() { { label: 'Overview', value: 'overview', content: ( -
-
Execution ID: {detail.executionId}
-
Status:
-
Route: {detail.routeId}
-
Duration: {detail.durationMs}ms
- {detail.errorMessage &&
Error: {detail.errorMessage}
} +
+
Details
+
+
+ Exchange ID + {detail.executionId} +
+
+ Status + +
+
+ Route + {detail.routeId} +
+
+ Duration + {detail.durationMs}ms +
+ {detail.errorMessage && ( +
+ Error + {detail.errorMessage} +
+ )} +
), }, @@ -85,7 +106,7 @@ export default function Dashboard() { return (
-
+
@@ -93,14 +114,22 @@ export default function Dashboard() {
- { setSelectedId(row.id); setProcessorIdx(null); }} - selectedId={selectedId ?? undefined} - sortable - pageSize={25} - /> +
+
+ Recent Exchanges +
+ {rows.length} results +
+
+ { setSelectedId(row.id); setProcessorIdx(null); }} + selectedId={selectedId ?? undefined} + sortable + pageSize={25} + /> +
-
- -
-
Status
-
- +
+
+
+ +
+ {id}
- - -
-
Duration
-
{detail.durationMs}ms
+
+
+
Duration
+
{detail.durationMs}ms
+
+
+
Route
+
{detail.routeId}
+
+
+
Application
+
{detail.groupName || 'unknown'}
+
- - -
-
Route
- {detail.routeId} -
-
- -
-
Application
- -
-
+
{detail.errorMessage && ( -
- - {detail.errorMessage} - -
+ + {detail.errorMessage} + )} -

Processor Timeline

- {processors.length > 0 ? ( - setSelectedProcessor(i)} - selectedIndex={selectedProcessor ?? undefined} - /> - ) : ( - No processor data available - )} +
+
+ Processor Timeline +
+
+ {processors.length > 0 ? ( + setSelectedProcessor(i)} + selectedIndex={selectedProcessor ?? undefined} + /> + ) : ( + No processor data available + )} +
+
{snapshot && ( -
-

Exchange Snapshot

-
- -
-

Input Body

+ <> +
Exchange Snapshot
+
+
+
+ Input Body +
+
- - -
-

Output Body

+
+
+
+ Output Body +
+
- +
-
- -
-

Input Headers

+
+
+
+ Input Headers +
+
- - -
-

Output Headers

+
+
+
+ Output Headers +
+
- +
-
+ )}
); diff --git a/ui/src/pages/Routes/RoutesMetrics.module.css b/ui/src/pages/Routes/RoutesMetrics.module.css new file mode 100644 index 00000000..ee56946f --- /dev/null +++ b/ui/src/pages/Routes/RoutesMetrics.module.css @@ -0,0 +1,63 @@ +.statStrip { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + margin-bottom: 16px; +} + +.tableSection { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + overflow: hidden; + margin-bottom: 20px; +} + +.tableHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.tableTitle { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.tableMeta { + font-size: 11px; + color: var(--text-muted); + font-family: var(--font-mono); +} + +.chartGrid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} + +.chartCard { + background: var(--bg-surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); + padding: 16px; + overflow: hidden; +} + +.chartTitle { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 12px; +} + +.rateGood { color: var(--success); } +.rateWarn { color: var(--warning); } +.rateBad { color: var(--error); } diff --git a/ui/src/pages/Routes/RoutesMetrics.tsx b/ui/src/pages/Routes/RoutesMetrics.tsx index 09055573..e5045d3c 100644 --- a/ui/src/pages/Routes/RoutesMetrics.tsx +++ b/ui/src/pages/Routes/RoutesMetrics.tsx @@ -8,6 +8,7 @@ import type { Column } from '@cameleer/design-system'; import { useRouteMetrics } from '../../api/queries/catalog'; import { useExecutionStats, useStatsTimeseries } from '../../api/queries/executions'; import { useGlobalFilters } from '@cameleer/design-system'; +import styles from './RoutesMetrics.module.css'; interface RouteRow { id: string; @@ -68,7 +69,11 @@ export default function RoutesMetrics() { { key: 'p99DurationMs', header: 'P99', sortable: true, render: (v) => `${(v as number).toFixed(0)}ms` }, { key: 'errorRate', header: 'Error Rate', sortable: true, - render: (v) => 0.05 ? 'var(--error)' : undefined }}>{((v as number) * 100).toFixed(1)}%, + render: (v) => { + const rate = v as number; + const cls = rate > 0.05 ? styles.rateBad : rate > 0.01 ? styles.rateWarn : styles.rateGood; + return {(rate * 100).toFixed(1)}%; + }, }, { key: 'sparkline', header: 'Trend', width: '80px', @@ -78,26 +83,44 @@ export default function RoutesMetrics() { return (
-
+
- +
+
+ Route Metrics + {rows.length} routes +
+ +
{chartData.length > 0 && ( -
- ({ x: i, y: d.throughput })) }]} height={200} /> - ({ x: i, y: d.latency })) }]} height={200} /> - ({ x: d.time as string, y: d.errors })) }]} height={200} /> - ({ x: i, y: d.successRate })) }]} height={200} /> +
+
+
Throughput
+ ({ x: i, y: d.throughput })) }]} height={200} /> +
+
+
Latency
+ ({ x: i, y: d.latency })) }]} height={200} /> +
+
+
Errors
+ ({ x: d.time as string, y: d.errors })) }]} height={200} /> +
+
+
Success Rate
+ ({ x: i, y: d.successRate })) }]} height={200} /> +
)}