feat: migrate UI to @cameleer/design-system, add backend endpoints
Some checks failed
CI / build (push) Failing after 47s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

Backend:
- Add agent_events table (V5) and lifecycle event recording
- Add route catalog endpoint (GET /routes/catalog)
- Add route metrics endpoint (GET /routes/metrics)
- Add agent events endpoint (GET /agents/events-log)
- Enrich AgentInstanceResponse with tps, errorRate, activeRoutes, uptimeSeconds
- Add TimescaleDB retention/compression policies (V6)

Frontend:
- Replace custom Mission Control UI with @cameleer/design-system components
- Rebuild all pages: Dashboard, ExchangeDetail, RoutesMetrics, AgentHealth,
  AgentInstance, RBAC, AuditLog, OIDC, DatabaseAdmin, OpenSearchAdmin, Swagger
- New LayoutShell with design system AppShell, Sidebar, TopBar, CommandPalette
- Consume design system from Gitea npm registry (@cameleer/design-system@0.0.1)
- Add .npmrc for scoped registry, update Dockerfile with REGISTRY_TOKEN arg

CI:
- Pass REGISTRY_TOKEN build-arg to UI Docker build step

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-19 17:38:39 +01:00
parent 82124c3145
commit 2b111c603c
150 changed files with 2750 additions and 21779 deletions

View File

@@ -0,0 +1,131 @@
import { useState, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router';
import {
Card, Badge, StatusDot, MonoText, CodeBlock, InfoCallout,
ProcessorTimeline, Breadcrumb, Spinner,
} from '@cameleer/design-system';
import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
export default function ExchangeDetail() {
const { id } = useParams();
const navigate = useNavigate();
const { data: detail, isLoading } = useExecutionDetail(id ?? null);
const [selectedProcessor, setSelectedProcessor] = useState<number | null>(null);
const { data: snapshot } = useProcessorSnapshot(id ?? null, selectedProcessor);
const processors = useMemo(() => {
if (!detail?.children) return [];
const result: any[] = [];
let offset = 0;
function walk(node: any) {
result.push({
name: node.processorId || node.processorType,
type: node.processorType,
durationMs: node.durationMs ?? 0,
status: node.status === 'COMPLETED' ? 'ok' : node.status === 'FAILED' ? 'fail' : 'ok',
startMs: offset,
});
offset += node.durationMs ?? 0;
if (node.children) node.children.forEach(walk);
}
detail.children.forEach(walk);
return result;
}, [detail]);
if (isLoading) return <div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}><Spinner size="lg" /></div>;
if (!detail) return <InfoCallout variant="warning">Exchange not found</InfoCallout>;
return (
<div>
<Breadcrumb items={[
{ label: 'Dashboard', href: '/apps' },
{ label: detail.groupName || 'App', href: `/apps/${detail.groupName}` },
{ label: id?.slice(0, 12) || '' },
]} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', margin: '1.5rem 0' }}>
<Card>
<div style={{ padding: '1rem' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Status</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.25rem' }}>
<StatusDot variant={detail.status === 'COMPLETED' ? 'success' : detail.status === 'FAILED' ? 'error' : 'running'} />
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} />
</div>
</div>
</Card>
<Card>
<div style={{ padding: '1rem' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Duration</div>
<div style={{ fontSize: '1.25rem', fontWeight: 600 }}>{detail.durationMs}ms</div>
</div>
</Card>
<Card>
<div style={{ padding: '1rem' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Route</div>
<MonoText>{detail.routeId}</MonoText>
</div>
</Card>
<Card>
<div style={{ padding: '1rem' }}>
<div style={{ fontSize: '0.75rem', color: 'var(--text-tertiary)' }}>Application</div>
<Badge label={detail.groupName || 'unknown'} color="auto" />
</div>
</Card>
</div>
{detail.errorMessage && (
<div style={{ marginBottom: '1.5rem' }}>
<InfoCallout variant="error">
{detail.errorMessage}
</InfoCallout>
</div>
)}
<h3 style={{ marginBottom: '0.75rem' }}>Processor Timeline</h3>
{processors.length > 0 ? (
<ProcessorTimeline
processors={processors}
totalMs={detail.durationMs}
onProcessorClick={(_p, i) => setSelectedProcessor(i)}
selectedIndex={selectedProcessor ?? undefined}
/>
) : (
<InfoCallout>No processor data available</InfoCallout>
)}
{snapshot && (
<div style={{ marginTop: '1.5rem' }}>
<h3 style={{ marginBottom: '0.75rem' }}>Exchange Snapshot</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<Card>
<div style={{ padding: '1rem' }}>
<h4 style={{ marginBottom: '0.5rem' }}>Input Body</h4>
<CodeBlock content={String(snapshot.inputBody ?? 'null')} />
</div>
</Card>
<Card>
<div style={{ padding: '1rem' }}>
<h4 style={{ marginBottom: '0.5rem' }}>Output Body</h4>
<CodeBlock content={String(snapshot.outputBody ?? 'null')} />
</div>
</Card>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem', marginTop: '1rem' }}>
<Card>
<div style={{ padding: '1rem' }}>
<h4 style={{ marginBottom: '0.5rem' }}>Input Headers</h4>
<CodeBlock content={JSON.stringify(snapshot.inputHeaders ?? {}, null, 2)} />
</div>
</Card>
<Card>
<div style={{ padding: '1rem' }}>
<h4 style={{ marginBottom: '0.5rem' }}>Output Headers</h4>
<CodeBlock content={JSON.stringify(snapshot.outputHeaders ?? {}, null, 2)} />
</div>
</Card>
</div>
</div>
)}
</div>
);
}