Unify route catalog (agent-driven) and apps table (deployment-driven) into a single catalog endpoint. Apps table becomes authoritative, agent data enriches with live health/routes. Slug-based URLs replace UUIDs for navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5.3 KiB
Catalog & App Consolidation Design
Problem
Two independent data structures represent "applications":
- Route Catalog — built from agent registry + ClickHouse stats. Uses string
applicationId(e.g.,"sample-app"). Contains live routes, agents, health, exchange counts. - Apps table — PostgreSQL CRUD. Uses UUID
id+ stringslug. Contains containerConfig, deployment info, JAR versions.
No bridge between them. The sidebar navigates to /apps/sample-app (catalog string) but the API expects /apps/{UUID}, causing 400 errors. The UI has separate useRouteCatalog() and useAllApps() hooks that never talk to each other.
Design Decisions
- Apps table is authoritative for "what applications exist" per environment
- Agent data enriches managed apps with live health, routes, metrics
- Unmanaged apps (agents without an App record) appear in the catalog with
managed: falseand a "Register" prompt on the Deployments tab - Slugs in URLs everywhere — no UUIDs in navigation
- Tab-aware sidebar stays as-is — the app tree just comes from one unified source
- Environment-scoped —
GET /api/v1/catalog?environment={slug}is the single source
Unified Catalog Endpoint
GET /api/v1/catalog?environment={envSlug}
Returns:
[
{
"slug": "sample-app",
"displayName": "Sample App",
"managed": true,
"environmentSlug": "default",
"health": "live",
"agentCount": 1,
"routes": [
{ "routeId": "route1", "name": "timer-route", "exchangeCount": 42, "routeState": "started" }
],
"exchangeCount": 42,
"deployment": {
"status": "RUNNING",
"replicas": "1/1",
"version": 1
}
}
]
Build logic
- Query
Apprecords filtered byenvironmentId(from env slug) - Query agent registry — filter by
environmentId, group byapplicationId - For each App record: find matching agent group by
App.slug == AgentInfo.applicationId, merge live data - For agent groups with no matching App record: add as unmanaged entry
- Enrich with ClickHouse exchange counts per app
CatalogApp fields
| Field | Source | Notes |
|---|---|---|
slug |
App.slug or AgentInfo.applicationId | Universal identifier |
displayName |
App.displayName or slug | Fallback to slug for unmanaged |
managed |
App record exists | boolean |
environmentSlug |
Environment.slug | From env lookup |
health |
Agent registry | live/stale/dead/offline (offline = no agents) |
agentCount |
Agent registry | Count of agents with matching applicationId |
routes |
Agent registry | Live routes from connected agents |
exchangeCount |
ClickHouse stats | Total exchanges |
deployment |
Deployment record | null if unmanaged or no deployment |
Slug-Based API Routing
All app-scoped controllers accept slug (String) instead of UUID:
GET /api/v1/apps/{slug}— app detailGET /api/v1/apps/{slug}/versions— JAR versionsPOST /api/v1/apps/{slug}/versions— upload JARGET /api/v1/apps/{slug}/deployments— deployment listPOST /api/v1/apps/{slug}/deployments— trigger deployPOST /api/v1/apps/{slug}/deployments/{deploymentId}/stop— stop
The controller resolves slug to App record via AppRepository.findByEnvironmentIdAndSlug(). Environment comes from the selected environment (query param or header).
DeploymentController path changes from /api/v1/apps/{appId}/deployments to /api/v1/apps/{slug}/deployments.
URL Routing (Frontend)
All app URLs use slug:
| Before | After |
|---|---|
/exchanges/{catalogAppId} |
/exchanges/{slug} (no change, already string) |
/dashboard/{catalogAppId} |
/dashboard/{slug} (no change) |
/apps/{UUID} |
/apps/{slug} |
/apps/{UUID}/deployments |
implicit in /apps/{slug} detail view |
Frontend Changes
New hook: useCatalog(environment)
Replaces both useRouteCatalog() and useAllApps().
export function useCatalog(environment?: string) {
return useQuery({
queryKey: ['catalog', environment],
queryFn: () => fetchCatalog(environment),
enabled: !!environment,
refetchInterval: 15_000,
});
}
Sidebar
buildAppTreeNodes() receives CatalogApp[] instead of SidebarApp[]. The SidebarApp.id becomes the slug. Navigation paths stay the same pattern: /exchanges/{slug}, etc.
AppsTab
AppDetailView receives slug from URL params. Looks up the app from the catalog data. If managed: true, shows deployments/versions/config. If managed: false, shows agent data + "Register this app" button.
AppListView uses catalog data filtered to the current environment, showing managed status as a badge.
What Gets Removed
RouteCatalogController— replaced byCatalogControllerAppCatalogEntryDTO — replaced byCatalogAppuseRouteCatalog()hook — replaced byuseCatalog()useAllApps()hook — replaced byuseCatalog()- Separate
SidebarApptype — catalog data is sufficient - UUID-based app URL navigation
What Stays
AppController— still needed for CRUD (create, update, delete apps)AppService,AppRepository— unchangedDeploymentController— path variable changes from UUID to slug- Agent registry — unchanged, still tracks live agents
- ClickHouse stats — unchanged, still queried for exchange counts
- Environment selector — unchanged, drives the catalog filter