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

@@ -32,8 +32,8 @@ import type { App, AppVersion, Deployment } from '../../api/queries/admin/apps';
import type { Environment } from '../../api/queries/admin/environments';
import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands';
import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands';
import { useRouteCatalog } from '../../api/queries/catalog';
import type { AppCatalogEntry, RouteSummary } from '../../api/types';
import { useCatalog } from '../../api/queries/catalog';
import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog';
import { DeploymentProgress } from '../../components/DeploymentProgress';
import styles from './AppsTab.module.css';
@@ -122,7 +122,7 @@ function AppListView({ selectedEnv, environments }: { selectedEnv: string | unde
<div className={styles.toolbar}>
<Button size="sm" variant="primary" onClick={() => navigate('/apps/new')}>+ Create App</Button>
</div>
<DataTable columns={columns} data={rows} onRowClick={(row) => navigate(`/apps/${row.id}`)} />
<DataTable columns={columns} data={rows} onRowClick={(row) => navigate(`/apps/${row.slug}`)} />
</div>
);
}
@@ -217,7 +217,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
// 2. Upload JAR
setStep('Uploading JAR...');
const version = await uploadJar.mutateAsync({ appId: app.id, file: file! });
const version = await uploadJar.mutateAsync({ appId: app.slug, file: file! });
// 3. Save container config
setStep('Saving configuration...');
@@ -234,7 +234,7 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
stripPathPrefix: stripPrefix,
sslOffloading: sslOffloading,
};
await updateContainerConfig.mutateAsync({ appId: app.id, config: containerConfig });
await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig });
// 4. Save agent config (will be pushed to agent on first connect)
setStep('Saving monitoring config...');
@@ -257,11 +257,11 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
// 5. Deploy (if requested)
if (deploy) {
setStep('Starting deployment...');
await createDeployment.mutateAsync({ appId: app.id, appVersionId: version.id, environmentId: envId });
await createDeployment.mutateAsync({ appId: app.slug, appVersionId: version.id, environmentId: envId });
}
toast({ title: deploy ? 'App created and deployed' : 'App created', description: name.trim(), variant: 'success' });
navigate(`/apps/${app.id}`);
navigate(`/apps/${app.slug}`);
} catch (e) {
toast({ title: 'Failed: ' + step, description: e instanceof Error ? e.message : 'Unknown error', variant: 'error', duration: 86_400_000 });
} finally {
@@ -476,13 +476,13 @@ function CreateAppView({ environments, selectedEnv }: { environments: Environmen
// DETAIL VIEW
// ═══════════════════════════════════════════════════════════════════
function AppDetailView({ appId, environments, selectedEnv }: { appId: string; environments: Environment[]; selectedEnv: string | undefined }) {
function AppDetailView({ appId: appSlug, environments, selectedEnv }: { appId: string; environments: Environment[]; selectedEnv: string | undefined }) {
const { toast } = useToast();
const navigate = useNavigate();
const { data: allApps = [] } = useAllApps();
const app = useMemo(() => allApps.find((a) => a.id === appId), [allApps, appId]);
const { data: versions = [] } = useAppVersions(appId);
const { data: deployments = [] } = useDeployments(appId);
const app = useMemo(() => allApps.find((a) => a.slug === appSlug), [allApps, appSlug]);
const { data: versions = [] } = useAppVersions(appSlug);
const { data: deployments = [] } = useDeployments(appSlug);
const uploadJar = useUploadJar();
const createDeployment = useCreateDeployment();
const stopDeployment = useStopDeployment();
@@ -502,7 +502,7 @@ function AppDetailView({ appId, environments, selectedEnv }: { appId: string; en
const file = e.target.files?.[0];
if (!file) return;
try {
const v = await uploadJar.mutateAsync({ appId, file });
const v = await uploadJar.mutateAsync({ appId: appSlug, file });
toast({ title: `Version ${v.version} uploaded`, description: file.name, variant: 'success' });
} catch { toast({ title: 'Upload failed', variant: 'error', duration: 86_400_000 }); }
if (fileInputRef.current) fileInputRef.current.value = '';
@@ -510,21 +510,21 @@ function AppDetailView({ appId, environments, selectedEnv }: { appId: string; en
async function handleDeploy(versionId: string, environmentId: string) {
try {
await createDeployment.mutateAsync({ appId, appVersionId: versionId, environmentId });
await createDeployment.mutateAsync({ appId: appSlug, appVersionId: versionId, environmentId });
toast({ title: 'Deployment started', variant: 'success' });
} catch { toast({ title: 'Deploy failed', variant: 'error', duration: 86_400_000 }); }
}
async function handleStop(deploymentId: string) {
try {
await stopDeployment.mutateAsync({ appId, deploymentId });
await stopDeployment.mutateAsync({ appId: appSlug, deploymentId });
toast({ title: 'Deployment stopped', variant: 'warning' });
} catch { toast({ title: 'Stop failed', variant: 'error', duration: 86_400_000 }); }
}
async function handleDelete() {
try {
await deleteApp.mutateAsync(appId);
await deleteApp.mutateAsync(appSlug);
toast({ title: 'App deleted', variant: 'warning' });
navigate('/apps');
} catch { toast({ title: 'Delete failed', variant: 'error', duration: 86_400_000 }); }
@@ -698,15 +698,15 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
const { data: agentConfig } = useApplicationConfig(app.slug);
const updateAgentConfig = useUpdateApplicationConfig();
const updateContainerConfig = useUpdateContainerConfig();
const { data: catalog } = useRouteCatalog();
const { data: catalog } = useCatalog();
const { data: processorToRoute = {} } = useProcessorRouteMapping(app.slug);
const isProd = environment?.production ?? false;
const [editing, setEditing] = useState(false);
const [configTab, setConfigTab] = useState<'variables' | 'monitoring' | 'traces' | 'recording' | 'resources'>('variables');
const appRoutes: RouteSummary[] = useMemo(() => {
const appRoutes: CatalogRoute[] = useMemo(() => {
if (!catalog) return [];
const entry = (catalog as AppCatalogEntry[]).find((e) => e.appId === app.slug);
const entry = (catalog as CatalogApp[]).find((e) => e.slug === app.slug);
return entry?.routes ?? [];
}, [catalog, app.slug]);
@@ -819,7 +819,7 @@ function ConfigSubTab({ app, environment }: { app: App; environment?: Environmen
sslOffloading: sslOffloading,
};
try {
await updateContainerConfig.mutateAsync({ appId: app.id, config: containerConfig });
await updateContainerConfig.mutateAsync({ appId: app.slug, config: containerConfig });
toast({ title: 'Configuration saved', variant: 'success' });
setEditing(false);
} catch { toast({ title: 'Failed to save container config', variant: 'error', duration: 86_400_000 }); }