feat: unified catalog endpoint and slug-based app navigation
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m19s
CI / docker (push) Successful in 1m7s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
SonarQube / sonarqube (push) Successful in 3m47s

Consolidate route catalog (agent-driven) and apps table (deployment-
driven) into a single GET /api/v1/catalog?environment={slug} endpoint.
Apps table is authoritative; agent data enriches with live health,
routes, and metrics. Unmanaged apps (agents without App record) appear
with managed=false.

- Add CatalogController merging App records + agent registry + ClickHouse
- Add CatalogApp DTO with deployment summary, managed flag, health
- Change AppController and DeploymentController to accept slugs (not UUIDs)
- Add AppRepository.findBySlug() and AppService.getBySlug()
- Replace useRouteCatalog() with useCatalog() across all UI components
- Navigate to /apps/{slug} instead of /apps/{UUID}
- Update sidebar, search, and all catalog lookups to use slug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-08 23:43:14 +02:00
parent 0720053523
commit b86e95f08e
15 changed files with 458 additions and 93 deletions

View File

@@ -21,7 +21,7 @@ import { Box, Settings, FileText, ChevronRight, Square, Pause, Star, X, User } f
import { AboutMeDialog } from './AboutMeDialog';
import css from './LayoutShell.module.css';
import { useQueryClient } from '@tanstack/react-query';
import { useRouteCatalog } from '../api/queries/catalog';
import { useCatalog } from '../api/queries/catalog';
import { useAgents } from '../api/queries/agents';
import { useSearchExecutions, useAttributeKeys } from '../api/queries/executions';
import { useUsers, useGroups, useRoles } from '../api/queries/admin/rbac';
@@ -56,24 +56,26 @@ function buildSearchData(
const results: SearchResult[] = [];
for (const app of catalog) {
const slug = app.slug || app.appId;
const name = app.displayName || slug;
const liveAgents = (app.agents || []).filter((a: any) => a.status === 'live').length;
results.push({
id: app.appId,
id: slug,
category: 'application',
title: app.appId,
title: name,
badges: [{ label: (app.health || 'unknown').toUpperCase(), color: healthToSearchColor(app.health) }],
meta: `${(app.routes || []).length} routes · ${(app.agents || []).length} agents (${liveAgents} live) · ${(app.exchangeCount ?? 0).toLocaleString()} exchanges`,
path: `/exchanges/${app.appId}`,
path: `/exchanges/${slug}`,
});
for (const route of (app.routes || [])) {
results.push({
id: `${app.appId}/${route.routeId}`,
id: `${slug}/${route.routeId}`,
category: 'route',
title: route.routeId,
badges: [{ label: app.appId }],
badges: [{ label: name }],
meta: `${(route.exchangeCount ?? 0).toLocaleString()} exchanges`,
path: `/exchanges/${app.appId}/${route.routeId}`,
path: `/exchanges/${slug}/${route.routeId}`,
});
}
}
@@ -288,7 +290,7 @@ function LayoutContent() {
const selectedEnv = useEnvironmentStore((s) => s.environment);
const setSelectedEnvRaw = useEnvironmentStore((s) => s.setEnvironment);
const { data: catalog } = useRouteCatalog(timeRange.start.toISOString(), timeRange.end.toISOString(), selectedEnv);
const { data: catalog } = useCatalog(selectedEnv);
const { data: allAgents } = useAgents(); // unfiltered — for environment discovery
const { data: agents } = useAgents(undefined, undefined, selectedEnv); // filtered — for sidebar/search
const { data: attributeKeys } = useAttributeKeys();
@@ -397,11 +399,11 @@ function LayoutContent() {
if (!catalog) return [];
const cmp = (a: string, b: string) => a.localeCompare(b);
return [...catalog]
.sort((a: any, b: any) => cmp(a.appId, b.appId))
.sort((a: any, b: any) => cmp(a.slug, b.slug))
.map((app: any) => ({
id: app.appId,
name: app.appId,
health: app.health as 'live' | 'stale' | 'dead',
id: app.slug,
name: app.displayName || app.slug,
health: (app.health === 'offline' ? 'dead' : app.health) as 'live' | 'stale' | 'dead',
exchangeCount: app.exchangeCount,
routes: [...(app.routes || [])]
.sort((a: any, b: any) => cmp(a.routeId, b.routeId))