# Orphaned Application Cleanup

## Problem

The sidebar application list merges three sources: PostgreSQL managed apps, in-memory agent registry, and ClickHouse stats. When agents disconnect and stop sending data, their applications persist in the sidebar as "ghost" entries — no live agents, low exchange counts, stale data. There is no mechanism to remove these orphaned entries. Test apps run once (`plain-local-test`, `quarkus-local-test`) clutter the sidebar indefinitely because ClickHouse stats have a 365-day TTL.

## Design

Two complementary mechanisms:

### 1. Auto-Cleanup (Time-Based)

The `CatalogController` already queries ClickHouse stats with a time range. For **discovered (non-managed) apps**, we tighten this: exclude apps that have no live agents AND no ClickHouse data newer than the discovery TTL.

- **Property:** `cameleer.server.catalog.discoveryttl` (default `7d`)
- **Logic:** When building the catalog response, for each discovered app (not in PostgreSQL `apps` table), check if it has live agents OR stats data within the TTL window. If neither, exclude it from the response.
- **Managed apps** (PostgreSQL records) always appear regardless of data age.
- **No background job** — the filtering is query-time. The app reappears if a new agent registers with the same `applicationId`.

### 2. Manual Dismiss (Immediate Purge)

For immediate removal when the user doesn't want to wait for auto-cleanup (test apps, decommissioned services, crashed infrastructure).

**Endpoint:** `DELETE /api/v1/catalog/{applicationId}` — ADMIN only.

**Precondition:** No live agents connected for this `applicationId`. Returns 409 Conflict if any exist.

**What it deletes:**

ClickHouse (all tables with `application_id`, scoped by `tenant_id`):
- `executions`
- `logs`
- `diagrams`
- `stats_1m_route`
- `stats_1m_app`
- `agent_events`
- `search_index`
- `usage_events`

All `ALTER TABLE DELETE` statements include `AND tenant_id = ?` to prevent cross-tenant data deletion in multi-tenant mode.

PostgreSQL (if a managed app record exists for this slug):
- `deployments` for this app
- `app_versions` for this app
- `apps` record

Filesystem:
- JAR files under `{jarStoragePath}/{appId}/`

Returns 204 on success.

**UI:** "Dismiss Application" button in the app detail view. Shown when no live agents are connected. Confirmation dialog shows what will be deleted (exchange count, log count). Adapts message for managed vs discovered apps.

### Confirmation Dialog

- Discovered app: "This will permanently delete all data (X exchanges, Y log entries) for '{appName}'. This cannot be undone."
- Managed app: "This will permanently delete the app, all versions, deployments, and all data (X exchanges, Y log entries) for '{appName}'. This cannot be undone."

## Files Changed

| Area | File | Change |
|------|------|--------|
| Config | `application.yml` | Add `cameleer.server.catalog.discoveryttl` |
| Core | New `CatalogCleanupService.java` | Purge logic: CH data deletion, PG app/version/deployment deletion, JAR cleanup |
| App | `CatalogController.java` | Filter discovered apps by TTL; add `DELETE /api/v1/catalog/{applicationId}` endpoint |
| App | `ClickHouseStatsStore.java` or new CH cleanup class | `deleteByApplicationId` across all CH tables |
| UI | App detail view | "Dismiss Application" button + confirmation dialog |
| UI | `catalog.ts` | New `useDismissApp()` mutation |

## Not in Scope

- No hidden-apps list, no undo, no blocklist
- No changes to how apps are discovered
- No changes to managed app CRUD (`/api/v1/apps` endpoints unchanged)
- No retention policy changes (ClickHouse TTL stays at 365 days)
