feat: unified catalog endpoint and slug-based app navigation
All checks were successful
All checks were successful
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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user