docs: add catalog consolidation design spec
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>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Catalog & App Consolidation Design
|
||||
|
||||
## Problem
|
||||
|
||||
Two independent data structures represent "applications":
|
||||
|
||||
1. **Route Catalog** — built from agent registry + ClickHouse stats. Uses string `applicationId` (e.g., `"sample-app"`). Contains live routes, agents, health, exchange counts.
|
||||
2. **Apps table** — PostgreSQL CRUD. Uses UUID `id` + string `slug`. 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: false` and 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:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"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
|
||||
|
||||
1. Query `App` records filtered by `environmentId` (from env slug)
|
||||
2. Query agent registry — filter by `environmentId`, group by `applicationId`
|
||||
3. For each App record: find matching agent group by `App.slug == AgentInfo.applicationId`, merge live data
|
||||
4. For agent groups with no matching App record: add as unmanaged entry
|
||||
5. 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 detail
|
||||
- `GET /api/v1/apps/{slug}/versions` — JAR versions
|
||||
- `POST /api/v1/apps/{slug}/versions` — upload JAR
|
||||
- `GET /api/v1/apps/{slug}/deployments` — deployment list
|
||||
- `POST /api/v1/apps/{slug}/deployments` — trigger deploy
|
||||
- `POST /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()`.
|
||||
|
||||
```typescript
|
||||
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 by `CatalogController`
|
||||
- `AppCatalogEntry` DTO — replaced by `CatalogApp`
|
||||
- `useRouteCatalog()` hook — replaced by `useCatalog()`
|
||||
- `useAllApps()` hook — replaced by `useCatalog()`
|
||||
- Separate `SidebarApp` type — catalog data is sufficient
|
||||
- UUID-based app URL navigation
|
||||
|
||||
## What Stays
|
||||
|
||||
- `AppController` — still needed for CRUD (create, update, delete apps)
|
||||
- `AppService`, `AppRepository` — unchanged
|
||||
- `DeploymentController` — 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
|
||||
Reference in New Issue
Block a user