feat: add orphaned app cleanup — auto-filter stale discovered apps, manual dismiss with data purge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { config } from '../../config';
|
||||
import { useAuthStore } from '../../auth/auth-store';
|
||||
import { useRefreshInterval } from './use-refresh-interval';
|
||||
@@ -61,6 +61,27 @@ export function useCatalog(environment?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useDismissApp() {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (applicationId: string) => {
|
||||
const token = useAuthStore.getState().accessToken;
|
||||
const res = await fetch(`${config.apiBaseUrl}/catalog/${applicationId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'X-Cameleer-Protocol-Version': '1',
|
||||
},
|
||||
});
|
||||
if (res.status === 409) throw new Error('Cannot dismiss — live agents are still connected');
|
||||
if (!res.ok) throw new Error(`Failed to dismiss: ${res.status}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['catalog'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRouteMetrics(from?: string, to?: string, appId?: string, environment?: string) {
|
||||
const refetchInterval = useRefreshInterval(30_000);
|
||||
return useQuery({
|
||||
|
||||
@@ -13,6 +13,8 @@ import logStyles from '../../styles/log-panel.module.css';
|
||||
import { useAgents, useAgentEvents } from '../../api/queries/agents';
|
||||
import { useApplicationLogs } from '../../api/queries/logs';
|
||||
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
|
||||
import { useCatalog, useDismissApp } from '../../api/queries/catalog';
|
||||
import { useIsAdmin } from '../../auth/auth-store';
|
||||
import { useEnvironmentStore } from '../../api/environment-store';
|
||||
import type { ConfigUpdateResponse } from '../../api/queries/commands';
|
||||
import type { AgentInstance } from '../../api/types';
|
||||
@@ -103,6 +105,12 @@ export default function AgentHealth() {
|
||||
const { data: appConfig } = useApplicationConfig(appId);
|
||||
const updateConfig = useUpdateApplicationConfig();
|
||||
|
||||
const isAdmin = useIsAdmin();
|
||||
const selectedEnvForCatalog = useEnvironmentStore((s) => s.environment);
|
||||
const { data: catalogApps } = useCatalog(selectedEnvForCatalog);
|
||||
const dismissApp = useDismissApp();
|
||||
const catalogEntry = catalogApps?.find((a) => a.slug === appId);
|
||||
|
||||
const [configEditing, setConfigEditing] = useState(false);
|
||||
const [configDraft, setConfigDraft] = useState<Record<string, string | boolean>>({});
|
||||
|
||||
@@ -468,6 +476,46 @@ export default function AgentHealth() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dismiss application card — shown when app-scoped, no agents, admin */}
|
||||
{appId && agentList.length === 0 && isAdmin && (
|
||||
<div className={`${sectionStyles.section}`} style={{ marginBottom: 16, padding: '16px 20px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<strong>No agents connected</strong>
|
||||
{catalogEntry && (
|
||||
<span style={{ marginLeft: 8, color: 'var(--text-muted)', fontSize: 13 }}>
|
||||
{catalogEntry.managed ? 'Managed app' : 'Discovered app'} — {catalogEntry.exchangeCount.toLocaleString()} exchanges recorded
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
disabled={dismissApp.isPending}
|
||||
onClick={() => {
|
||||
const count = catalogEntry?.exchangeCount ?? 0;
|
||||
const ok = window.confirm(
|
||||
`Dismiss "${appId}" and permanently delete all data (${count.toLocaleString()} exchanges)?\n\nThis action cannot be undone.`
|
||||
);
|
||||
if (ok) {
|
||||
dismissApp.mutate(appId, {
|
||||
onSuccess: () => {
|
||||
toast({ title: 'Application dismissed', description: `${appId} and all associated data have been deleted`, variant: 'success' });
|
||||
navigate('/runtime');
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({ title: 'Dismiss failed', description: err.message, variant: 'error', duration: 86_400_000 });
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{dismissApp.isPending ? 'Dismissing\u2026' : 'Dismiss Application'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Group cards grid */}
|
||||
<div className={isFullWidth ? styles.groupGridSingle : styles.groupGrid}>
|
||||
{groups.map((group) => (
|
||||
|
||||
Reference in New Issue
Block a user